diff --git a/Cargo.lock b/Cargo.lock index 3ae3d5fb2386..702b3f565342 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1600,7 +1600,6 @@ dependencies = [ "bun_collections", "bun_core", "bun_http_types", - "bun_js_parser", "bun_options_types", "bun_paths", "bun_ptr", @@ -2136,6 +2135,7 @@ dependencies = [ "bun_alloc", "bun_collections", "bun_core", + "bun_opaque", "bun_paths", "bun_wyhash", "const_format", @@ -2269,7 +2269,6 @@ dependencies = [ "bun_alloc", "bun_collections", "bun_core", - "bun_io", "bun_zlib_sys", "const_format", "enum-map", diff --git a/src/ast/lexer_log.rs b/src/ast/lexer_log.rs index 7346a5ee2ad8..9f25dfe124ac 100644 --- a/src/ast/lexer_log.rs +++ b/src/ast/lexer_log.rs @@ -6,6 +6,10 @@ //! `prev_error_loc`, push into `Log`, then record the loc. This trait //! collapses all three. //! +//! It also hosts the shared string escape-sequence decoder +//! ([`decode_escape_sequences`] / [`EscapeLexer`]) that the js/json and toml +//! lexers previously each carried a ~330-line copy of. +//! //! The trait carries a `'s` lifetime so `source()` can hand back the lexer's //! stored `&'s Source` *without* borrowing `self` — that is what lets the //! provided bodies call `self.log_mut()` afterwards without a split-borrow @@ -111,3 +115,408 @@ pub trait LexerLog<'s> { Err(Self::syntax_err()) } } + +/// Surface [`decode_escape_sequences`] needs from a lexer. Monomorphizes per +/// lexer type, so codegen matches the previous per-lexer inline copies. +pub trait EscapeLexer<'s>: LexerLog<'s> { + /// Decoded output sink: UTF-16 code units for the js lexer, WTF-8 bytes + /// for the toml lexer. + type Buf; + + /// JSON mode: reject legacy octal, `\u{...}`, line continuations, and any + /// simple escape outside the JSON set. + const IS_JSON: bool = false; + + /// toml only: keep error spans in their historical shape — the legacy + /// octal `Range` start is text-relative (no `start +`) and the `\u{...}` + /// span start also subtracts the width of `{`. The js lexer computes both + /// absolutely (oven-sh/bun#31134). + const LEGACY_ERROR_SPANS: bool = false; + + fn end_mut(&mut self) -> &mut usize; + fn push_codepoint(buf: &mut Self::Buf, c: u32); +} + +/// Decodes the backslash escape sequences of a string-literal body `text` +/// into `buf`. `start` is the absolute source offset of `text`'s first byte, +/// used to report error locations. +/// +/// `ALLOW_LINE_CONTINUATIONS` permits `\` (always true for js; +/// toml multiline basic strings only). `REJECT_HEX_ESCAPE` errors on `\x` +/// (toml multiline basic strings only). +pub fn decode_escape_sequences< + 's, + L: EscapeLexer<'s>, + const ALLOW_LINE_CONTINUATIONS: bool, + const REJECT_HEX_ESCAPE: bool, +>( + lexer: &mut L, + start: usize, + text: &[u8], + buf: &mut L::Buf, +) -> Result<(), L::Err> { + use bun_core::fmt::hex_digit_value_u32; + use bun_core::strings; + use bun_core::strings::CodePoint; + + let iterator = strings::CodepointIterator::init(text); + let mut iter = strings::Cursor::default(); + while iterator.next(&mut iter) { + let width = iter.width; + match iter.c { + 0x0D => { + // From the specification: + // + // 11.8.6.1 Static Semantics: TV and TRV + // + // TV excludes the code units of LineContinuation while TRV includes + // them. and LineTerminatorSequences are normalized to + // for both TV and TRV. An explicit EscapeSequence is needed to + // include a or sequence. + + // Convert '\r\n' into '\n'. After `next()` returns for `\r`, + // `iter.i` is the start byte of the `\r` itself — the `\n` we're + // looking for is at `iter.i + 1`. + let next_i: usize = iter.i as usize + 1; + iter.i += (next_i < text.len() && text[next_i] == b'\n') as u32; + + // Convert '\r' into '\n' + L::push_codepoint(buf, u32::from(b'\n')); + continue; + } + + 0x5C => { + if !iterator.next(&mut iter) { + return Ok(()); + } + + let c2 = iter.c; + let width2 = iter.width; + match c2 { + // https://mathiasbynens.be/notes/javascript-escapes#single + 0x62 => { + L::push_codepoint(buf, 0x08); + continue; + } + 0x66 => { + L::push_codepoint(buf, 0x0C); + continue; + } + 0x6E => { + L::push_codepoint(buf, 0x0A); + continue; + } + 0x76 => { + // Vertical tab is invalid JSON + // We're going to allow it. + L::push_codepoint(buf, 0x0B); + continue; + } + 0x74 => { + L::push_codepoint(buf, 0x09); + continue; + } + 0x72 => { + L::push_codepoint(buf, 0x0D); + continue; + } + + // legacy octal literals + 0x30..=0x37 => { + let octal_start = (iter.i as usize + width2 as usize).saturating_sub(2); + if L::IS_JSON { + *lexer.end_mut() = + (start + iter.i as usize).saturating_sub(width2 as usize); + lexer.syntax_error()?; + } + + // 1-3 digit octal + let mut is_bad = false; + let mut value: i64 = (c2 - 0x30) as i64; + let mut prev = iter; + + if !iterator.next(&mut iter) { + if value == 0 { + L::push_codepoint(buf, 0); + return Ok(()); + } + lexer.syntax_error()?; + return Ok(()); + } + + let c3: CodePoint = iter.c; + + match c3 { + 0x30..=0x37 => { + value = value * 8 + (c3 - 0x30) as i64; + prev = iter; + if !iterator.next(&mut iter) { + return lexer.syntax_error(); + } + + let c4 = iter.c; + match c4 { + 0x30..=0x37 => { + let temp = value * 8 + (c4 - 0x30) as i64; + if temp < 256 { + value = temp; + } else { + iter = prev; + } + } + 0x38 | 0x39 => { + is_bad = true; + } + _ => { + iter = prev; + } + } + } + 0x38 | 0x39 => { + is_bad = true; + } + _ => { + iter = prev; + } + } + + iter.c = i32::try_from(value).expect("int cast"); + if is_bad { + // `octal_start` is text-relative like `iter.i`; map back + // to an absolute source position the same way every + // sibling error path does (e.g. `start + hex_start` in + // the `\u{}` branch) — unless the lexer keeps its + // historical text-relative span. + let range_start = if L::LEGACY_ERROR_SPANS { + octal_start + } else { + start + octal_start + }; + // `add_range_error` has no failing path; `?` keeps the + // signature free of a `Debug` bound on `L::Err`. + lexer.add_range_error( + Range { + loc: Loc { + start: i32::try_from(range_start).expect("int cast"), + }, + len: i32::try_from(iter.i as usize - octal_start) + .expect("int cast"), + }, + format_args!("Invalid legacy octal literal"), + )?; + } + } + 0x38 | 0x39 => { + iter.c = c2; + } + // 2-digit hexadecimal + 0x78 => { + if REJECT_HEX_ESCAPE { + *lexer.end_mut() = + (start + iter.i as usize).saturating_sub(width2 as usize); + lexer.syntax_error()?; + } + + let mut value: CodePoint = 0; + let mut c3: CodePoint; + let mut width3: u8; + + if !iterator.next(&mut iter) { + return lexer.syntax_error(); + } + c3 = iter.c; + width3 = iter.width; + match hex_digit_value_u32(c3 as u32) { + Some(d) => value = (value * 16) | d as CodePoint, + None => { + *lexer.end_mut() = + (start + iter.i as usize).saturating_sub(width3 as usize); + return lexer.syntax_error(); + } + } + + if !iterator.next(&mut iter) { + return lexer.syntax_error(); + } + c3 = iter.c; + width3 = iter.width; + match hex_digit_value_u32(c3 as u32) { + Some(d) => value = (value * 16) | d as CodePoint, + None => { + *lexer.end_mut() = + (start + iter.i as usize).saturating_sub(width3 as usize); + return lexer.syntax_error(); + } + } + + iter.c = value; + } + 0x75 => { + // We're going to make this an i64 so we don't risk integer overflows + // when people do weird things + let mut value: i64 = 0; + + if !iterator.next(&mut iter) { + return lexer.syntax_error(); + } + let mut c3 = iter.c; + let mut width3 = iter.width; + + // variable-length + if c3 == 0x7B { + if L::IS_JSON { + *lexer.end_mut() = + (start + iter.i as usize).saturating_sub(width2 as usize); + lexer.syntax_error()?; + } + + // `iter.i` is the byte offset of `{` inside `text`; + // back up past `\` and `u` only. `width3` is the + // width of `{` itself, which `iter.i` already points + // at — subtracting it lands one character too early + // (kept for lexers with `LEGACY_ERROR_SPANS`). + let mut hex_start = (iter.i as usize) + .saturating_sub(width as usize) + .saturating_sub(width2 as usize); + if L::LEGACY_ERROR_SPANS { + hex_start = hex_start.saturating_sub(width3 as usize); + } + let mut is_first = true; + let mut is_out_of_range = false; + 'variable_length: loop { + if !iterator.next(&mut iter) { + break 'variable_length; + } + c3 = iter.c; + + if c3 == 0x7D { + if is_first { + *lexer.end_mut() = (start + iter.i as usize) + .saturating_sub(width3 as usize); + return lexer.syntax_error(); + } + break 'variable_length; + } + match hex_digit_value_u32(c3 as u32) { + Some(d) => value = (value * 16) | d as i64, + None => { + *lexer.end_mut() = (start + iter.i as usize) + .saturating_sub(width3 as usize); + return lexer.syntax_error(); + } + } + + // '\U0010FFFF + // copied from golang utf8.MaxRune + if value > 1_114_111 { + is_out_of_range = true; + } + is_first = false; + } + + if is_out_of_range { + lexer.add_range_error( + Range { + loc: Loc { + start: i32::try_from(start + hex_start) + .expect("int cast"), + }, + len: i32::try_from( + (iter.i as usize).saturating_sub(hex_start), + ) + .unwrap(), + }, + format_args!("Unicode escape sequence is out of range"), + )?; + + return Ok(()); + } + + // fixed-length + } else { + // Fixed-length + let mut j: usize = 0; + while j < 4 { + match hex_digit_value_u32(c3 as u32) { + Some(d) => value = (value * 16) | d as i64, + None => { + *lexer.end_mut() = (start + iter.i as usize) + .saturating_sub(width3 as usize); + return lexer.syntax_error(); + } + } + + if j < 3 { + if !iterator.next(&mut iter) { + return lexer.syntax_error(); + } + c3 = iter.c; + width3 = iter.width; + } + j += 1; + } + let _ = width3; + } + + iter.c = value as CodePoint; // @truncate + } + 0x0D => { + if L::IS_JSON { + *lexer.end_mut() = + (start + iter.i as usize).saturating_sub(width2 as usize); + lexer.syntax_error()?; + } else if !ALLOW_LINE_CONTINUATIONS { + *lexer.end_mut() = + (start + iter.i as usize).saturating_sub(width2 as usize); + lexer.add_default_error(b"Unexpected end of line")?; + } + + // Make sure Windows CRLF counts as a single newline. + // Guard on the index we actually read (`iter.i + 1`), not + // `iter.i` — a string ending in `\` would otherwise + // read `text[len]`. + let next_i: usize = iter.i as usize + 1; + iter.i += (next_i < text.len() && text[next_i] == b'\n') as u32; + + // Ignore line continuations. A line continuation is not an escaped newline. + continue; + } + 0x0A | 0x2028 | 0x2029 => { + if L::IS_JSON { + *lexer.end_mut() = + (start + iter.i as usize).saturating_sub(width2 as usize); + lexer.syntax_error()?; + } else if !ALLOW_LINE_CONTINUATIONS { + *lexer.end_mut() = + (start + iter.i as usize).saturating_sub(width2 as usize); + lexer.add_default_error(b"Unexpected end of line")?; + } + + // Ignore line continuations. A line continuation is not an escaped newline. + continue; + } + _ => { + if L::IS_JSON { + match c2 { + 0x22 | 0x5C | 0x2F => {} + _ => { + *lexer.end_mut() = + (start + iter.i as usize).saturating_sub(width2 as usize); + lexer.syntax_error()?; + } + } + } + iter.c = c2; + } + } + } + _ => {} + } + + match iter.c { + -1 => return lexer.add_default_error(b"Unexpected end of file"), + c => L::push_codepoint(buf, c as u32), + } + } + Ok(()) +} diff --git a/src/bun_core/output.rs b/src/bun_core/output.rs index 703dc80dcb2d..999d2d54e87d 100644 --- a/src/bun_core/output.rs +++ b/src/bun_core/output.rs @@ -1746,16 +1746,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}; @@ -2057,82 +2047,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 01c10021cde5..a27181b07e36 100644 --- a/src/bun_core/string/mod.rs +++ b/src/bun_core/string/mod.rs @@ -2438,8 +2438,12 @@ 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 + // (`write_pre_quoted_string_inner`, `bun_js_printer::estimate_length_for_utf8`) + // collapse to a single monomorphization instead of one per + // (ascii_only × quote_char × …) combo — see `write_pre_quoted_string_inner`. #[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 @@ -2456,9 +2460,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, @@ -2467,68 +2482,119 @@ pub mod printer { json: bool, encoding: StrEncoding, ) -> Result<(), crate::Error> { - 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, + ) -> Result<(), crate::Error> + 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")?; @@ -2572,11 +2638,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"$")?; } @@ -2588,6 +2659,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]])?; @@ -2609,9 +2681,22 @@ pub mod printer { bytes: &mut MutableString, ascii_only: bool, ) -> Result<(), crate::Error> { - // PERF: consider pre-growing via an estimated UTF-8 length — profile if it shows up on a hot path. + // `ascii_only` is threaded at runtime so + // the heavy escaper isn't monomorphized per ascii_only/quote-char combo. + // + // Heuristic reservation (~12.5% slack) instead of a full + // escaped-length pre-scan, which would do a SIMD scan + per-escape rune + // decode over `text` just to size the buffer — the same work + // `write_pre_quoted_string_inner` repeats immediately below. + // Tab-indented JS (e.g. three.js) has ~9.4% of bytes needing 2-byte + // escapes (tabs + newlines + quotes/backslashes), so 6.25% slack would + // under-shoot and force a 2x doubling memcpy of the whole source. The + // 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..5d11753d8134 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 5-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/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index a622920a50b2..6c3e13cd53cd 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -2483,6 +2483,45 @@ pub mod bv2_impl { } } + /// Shared tail of every parse-task enqueue site: set the scheduling + /// fields, then either hand the task to an onLoad plugin or schedule it + /// on the worker pool, registering copy-for-bundling loaders (`file`, + /// etc.) as additional files with no side effects. + /// + /// `task.jsx` is deliberately left to the caller — the enqueue sites + /// differ in whether they keep the resolver's tsconfig-derived pragma + /// (syncing only `development`) or clone the target transpiler's pragma + /// wholesale. + fn configure_and_dispatch_parse_task( + &mut self, + task: &mut ParseTask, + loader: Loader, + target: options::Target, + is_entry_point: bool, + ) { + task.loader = Some(loader); + task.task.node.next = core::ptr::null_mut(); + task.io_task.node.next = core::ptr::null_mut(); + task.tree_shaking = self.linker.options.tree_shaking; + task.is_entry_point = is_entry_point; + task.known_target = target; + + if !self.enqueue_on_load_plugin_if_needed(task) { + if loader.should_copy_for_bundling() { + let source_index = task.source_index.get(); + let additional_files: &mut bun_alloc::AstVec = + &mut self.graph.input_files.items_additional_files_mut() + [source_index as usize]; + additional_files.push(crate::AdditionalFile::SourceIndex(source_index)); + self.graph.input_files.items_side_effects_mut()[source_index as usize] = + bun_ast::SideEffects::NoSideEffectsPureData; + self.graph.estimated_file_loader_count += 1; + } + + self.graph.pool().schedule(task); + } + } + pub fn enqueue_file_from_dev_server_incremental_graph_invalidation( &mut self, path_slice: &[u8], @@ -2538,10 +2577,6 @@ pub mod bv2_impl { let task_val = ParseTask::init(&result, source_index, self); // SAFETY: arena outlives the bundle pass; reborrow `*mut` as `&mut`. let task: &mut ParseTask = self.arena_create(task_val); - task.loader = Some(loader); - task.task.node.next = core::ptr::null_mut(); - task.tree_shaking = self.linker.options.tree_shaking; - task.known_target = target; { let t = self.transpiler_for_target(target); task.jsx.development = match t.options.force_node_env { @@ -2550,22 +2585,7 @@ pub mod bv2_impl { options::ForceNodeEnv::Unspecified => t.options.jsx.development, }; } - - // Handle onLoad plugins as entry points - if !self.enqueue_on_load_plugin_if_needed(task) { - if loader.should_copy_for_bundling() { - let additional_files: &mut bun_alloc::AstVec = - &mut self.graph.input_files.items_additional_files_mut() - [source_index.get() as usize]; - additional_files - .push(crate::AdditionalFile::SourceIndex(task.source_index.get())); - self.graph.input_files.items_side_effects_mut()[source_index.get() as usize] = - bun_ast::SideEffects::NoSideEffectsPureData; - self.graph.estimated_file_loader_count += 1; - } - - self.graph.pool().schedule(task); - } + self.configure_and_dispatch_parse_task(task, loader, target, false); Ok(()) } @@ -2647,11 +2667,6 @@ pub mod bv2_impl { let task_val = ParseTask::init(result, source_index, self); // SAFETY: arena outlives the bundle pass; reborrow `*mut` as `&mut`. let task: &mut ParseTask = self.arena_create(task_val); - task.loader = Some(loader); - task.task.node.next = core::ptr::null_mut(); - task.tree_shaking = self.linker.options.tree_shaking; - task.is_entry_point = is_entry_point; - task.known_target = target; { let bundler = self.transpiler_for_target(target); task.jsx.development = match bundler.options.force_node_env { @@ -2660,22 +2675,7 @@ pub mod bv2_impl { options::ForceNodeEnv::Unspecified => bundler.options.jsx.development, }; } - - // Handle onLoad plugins as entry points - if !self.enqueue_on_load_plugin_if_needed(task) { - if loader.should_copy_for_bundling() { - let additional_files: &mut bun_alloc::AstVec = - &mut self.graph.input_files.items_additional_files_mut() - [source_index.get() as usize]; - additional_files - .push(crate::AdditionalFile::SourceIndex(task.source_index.get())); - self.graph.input_files.items_side_effects_mut()[source_index.get() as usize] = - bun_ast::SideEffects::NoSideEffectsPureData; - self.graph.estimated_file_loader_count += 1; - } - - self.graph.pool().schedule(task); - } + self.configure_and_dispatch_parse_task(task, loader, target, is_entry_point); self.graph .entry_points @@ -3486,30 +3486,11 @@ pub mod bv2_impl { ); // SAFETY: arena outlives the bundle pass; reborrow `*mut` as `&mut`. let task: &mut ParseTask = self.arena_create(task_val); - task.loader = Some(loader); task.jsx = self.transpiler_for_target(known_target).options.jsx.clone(); - task.task.node.next = core::ptr::null_mut(); - task.io_task.node.next = core::ptr::null_mut(); - task.tree_shaking = self.linker.options.tree_shaking; - task.known_target = known_target; self.increment_scan_counter(); - // Handle onLoad plugins - if !self.enqueue_on_load_plugin_if_needed(task) { - if loader.should_copy_for_bundling() { - let additional_files: &mut bun_alloc::AstVec = - &mut self.graph.input_files.items_additional_files_mut() - [source_index.get() as usize]; - additional_files - .push(crate::AdditionalFile::SourceIndex(task.source_index.get())); - self.graph.input_files.items_side_effects_mut()[source_index.get() as usize] = - bun_ast::SideEffects::NoSideEffectsPureData; - self.graph.estimated_file_loader_count += 1; - } - - self.graph.pool().schedule(task); - } + self.configure_and_dispatch_parse_task(task, loader, known_target, false); Ok(source_index.get()) } @@ -3563,7 +3544,6 @@ pub mod bv2_impl { } else { self.transpiler_for_target(known_target).options.jsx.clone() }; - let tree_shaking = self.linker.options.tree_shaking; // SAFETY: arena (`self.graph.heap`) outlives the bundle pass; coerce the // `&mut ParseTask` to `*mut` immediately so the `&self` borrow from // `arena()` ends before we take `&mut self` below. @@ -3576,9 +3556,6 @@ pub mod bv2_impl { module_type: options::ModuleType::Unknown, emit_decorator_metadata: false, // TODO package_version: bun_ast::StoreStr::EMPTY, - loader: Some(loader), - tree_shaking, - known_target, ..Default::default() }); // SAFETY: `task` was just arena-allocated above; no other references exist yet. @@ -3587,27 +3564,17 @@ pub mod bv2_impl { (*task).ctx = Some(bun_ptr::ParentRef::from_raw_mut( std::ptr::from_mut(self).cast::>(), )); - (*task).task.node.next = core::ptr::null_mut(); - (*task).io_task.node.next = core::ptr::null_mut(); } self.increment_scan_counter(); - // Handle onLoad plugins // SAFETY: `task` lives in the bundle-pass arena; sole reference until scheduled. - if !self.enqueue_on_load_plugin_if_needed(unsafe { &mut *task }) { - if loader.should_copy_for_bundling() { - let additional_files: &mut bun_alloc::AstVec = - &mut self.graph.input_files.items_additional_files_mut() - [source_index.get() as usize]; - additional_files.push(crate::AdditionalFile::SourceIndex(source_index.get())); - self.graph.input_files.items_side_effects_mut()[source_index.get() as usize] = - bun_ast::SideEffects::NoSideEffectsPureData; - self.graph.estimated_file_loader_count += 1; - } - - self.graph.pool().schedule(task); - } + self.configure_and_dispatch_parse_task( + unsafe { &mut *task }, + loader, + known_target, + false, + ); Ok(source_index.get()) } @@ -4667,35 +4634,19 @@ pub mod bv2_impl { .clone(), source_index: bun_ast::Index::init(source_index.get()), module_type: options::ModuleType::Unknown, - loader: Some(loader), - tree_shaking: this.linker.options.tree_shaking, - known_target: resolve.import_record.original_target, ..Default::default() }; // Arena-owned. // SAFETY: arena outlives the bundle pass. let task: &mut ParseTask = this.arena_create(task_val); - task.task.node.next = core::ptr::null_mut(); - task.io_task.node.next = core::ptr::null_mut(); this.increment_scan_counter(); - if !this.enqueue_on_load_plugin_if_needed(task) { - if loader.should_copy_for_bundling() { - let additional_files: &mut bun_alloc::AstVec< - crate::AdditionalFile, - > = &mut this.graph.input_files.items_additional_files_mut() - [source_index.get() as usize]; - additional_files.push(crate::AdditionalFile::SourceIndex( - task.source_index.get(), - )); - this.graph.input_files.items_side_effects_mut() - [source_index.get() as usize] = - bun_ast::SideEffects::NoSideEffectsPureData; - this.graph.estimated_file_loader_count += 1; - } - - this.graph.pool().schedule(task); - } + this.configure_and_dispatch_parse_task( + task, + loader, + resolve.import_record.original_target, + false, + ); } else { // SAFETY: map slot from `get_or_put` above; map not mutated since. out_source_index = Some(Index::init(unsafe { *value_ptr })); diff --git a/src/bundler/cache.rs b/src/bundler/cache.rs index aee00128dd51..856386431bbc 100644 --- a/src/bundler/cache.rs +++ b/src/bundler/cache.rs @@ -1,11 +1,8 @@ use core::sync::atomic::{AtomicBool, Ordering}; use bun_alloc::Arena as Bump; -use bun_core::{self, Global, Output, ZStr, feature_flags}; -use bun_core::{MutableString, strings}; +use bun_core::{self, Global, strings}; use bun_js_parser as js_parser; -use bun_resolver::fs as fs_mod; -use bun_sys::{self, Fd}; // B-3 UNIFIED: `Define` is now the single canonical `bun_js_parser::defines::Define` // (re-exported via `crate::defines`); `JavaScript::parse`/`scan` and the bundler's @@ -157,40 +154,14 @@ impl Set { pub fn init(_arena: &Bump) -> Set { Set { js: JavaScript::init(), - fs: Fs { - shared_buffer: MutableString::init(0).expect("unreachable"), - macro_shared_buffer: MutableString::init(0).expect("unreachable"), - use_alternate_source_cache: false, - stream: false, - }, + fs: Fs::default(), json: Json::init(), } } } -bun_core::declare_scope!(fs, visible); - -pub struct Fs { - pub shared_buffer: MutableString, - pub macro_shared_buffer: MutableString, - - pub use_alternate_source_cache: bool, - pub stream: bool, -} - -impl Default for Fs { - fn default() -> Self { - Self { - shared_buffer: MutableString::init(0).expect("unreachable"), - macro_shared_buffer: MutableString::init(0).expect("unreachable"), - use_alternate_source_cache: false, - stream: false, - } - } -} - // ══════════════════════════════════════════════════════════════════════════ -// `Entry`/`Contents`/`ExternalFreeFunction` are defined +// `Fs`/`Entry`/`Contents`/`ExternalFreeFunction` are defined // canonically in `bun_resolver::cache` (lower tier) because `Resolver.caches` // is typed by them and the resolver crate cannot depend on the bundler. // Re-export here so `crate::cache::Entry` and `bun_resolver::cache::Entry` @@ -198,266 +169,11 @@ impl Default for Fs { // a resolver-produced `Entry` and hands it to bundler-typed consumers without // a structural shim. See src/resolver/lib.rs `pub mod cache`. // ══════════════════════════════════════════════════════════════════════════ -pub use bun_resolver::cache::{Contents, Entry, ExternalFreeFunction}; +pub use bun_resolver::cache::{Contents, Entry, ExternalFreeFunction, Fs}; /// Legacy alias — several call sites import `crate::cache::CacheEntry`. pub type CacheEntry = Entry; -impl Fs { - // When we are in a macro, the shared buffer may be in use by the in-progress macro. - // so we have to dynamically switch it out. - #[inline] - pub fn shared_buffer(&mut self) -> &mut MutableString { - if !self.use_alternate_source_cache { - &mut self.shared_buffer - } else { - &mut self.macro_shared_buffer - } - } - - /// When we need to suspend/resume something that has pointers into the shared buffer, we need to - /// switch out the shared buffer so that it is not in use. - /// - /// Ownership transfer: the old buffer must NOT be freed here, because the - /// suspended parse keeps pointers into it (the shared buffer becomes owned - /// by the AsyncModule struct in the module loader). Plain - /// field assignment would drop+free the old buffer → use-after-free on resume. So we return - /// the detached buffer; the caller MUST take ownership of it and keep it alive for as long as - /// `parse_result.source.contents` may be read. - pub fn reset_shared_buffer(&mut self, buffer: *const MutableString) -> MutableString { - if core::ptr::eq(buffer, &raw const self.shared_buffer) { - core::mem::replace(&mut self.shared_buffer, MutableString::init_empty()) - } else if core::ptr::eq(buffer, &raw const self.macro_shared_buffer) { - core::mem::replace(&mut self.macro_shared_buffer, MutableString::init_empty()) - } else { - unreachable!("resetSharedBuffer: invalid buffer"); - } - } - - // No Drop impl needed beyond the auto-drop of `shared_buffer` / - // `macro_shared_buffer`. -} - -// File reads route through the canonical `bun_resolver::fs::read_file_contents` -// (one body for the stat→grow→pread-loop→BOM-strip path); these methods only -// handle open/seek/close around it. -impl Fs { - /// Read `path` into the caller's `shared` buffer (HMR / dev-server path). - pub fn read_file_shared( - &mut self, - _fs: &mut fs_mod::FileSystem, - path: &ZStr, - cached_file_descriptor: Option, - shared: &mut MutableString, - ) -> Result { - let rfs = &_fs.fs; - - let mut owned: Option = None; - let fd: Fd = if let Some(fd) = cached_file_descriptor { - // `try handle.seekTo(0)` — rewind a cached fd before re-reading. - bun_sys::File::borrow(&fd) - .seek_to(0) - .map_err(bun_core::Error::from)?; - fd - } else { - let f = bun_sys::open_file_absolute_z(path, bun_sys::OpenFlags::READ_ONLY) - .map_err(bun_core::Error::from)?; - let raw = f.handle(); - owned = Some(f); - raw - }; - let file_handle = bun_sys::File::borrow(&fd); - - let contents = match fs_mod::read_file_contents( - file_handle, - path.as_bytes(), - true, - shared, - self.stream, - ) - .map(Contents::from) - { - Ok(c) => c, - Err(err) => { - if cfg!(debug_assertions) { - Output::print_error(format_args!( - "{}: readFile error -- {}", - bstr::BStr::new(path.as_bytes()), - bstr::BStr::new(err.name()), - )); - } - return Err(err); - } - }; - - let will_close = cached_file_descriptor.is_none() && rfs.need_to_close_files(); - let publish_fd = feature_flags::STORE_FILE_DESCRIPTORS && !will_close; - if publish_fd { - if let Some(f) = owned.take() { - let _ = f.into_raw(); - } - } - Ok(Entry { - contents, - fd: if publish_fd { fd } else { Fd::INVALID }, - external_free_function: ExternalFreeFunction::NONE, - }) - } - - pub fn read_file( - &mut self, - _fs: &mut fs_mod::FileSystem, - path: &[u8], - dirname_fd: Fd, - use_shared_buffer: bool, - _file_handle: Option, - ) -> Result { - self.read_file_with_allocator(_fs, path, dirname_fd, use_shared_buffer, _file_handle, None) - } - - /// `use_shared_buffer` is taken at runtime — the live - /// callers (`ParseTask::get_code_for_parse_task_without_plugins`, - /// `Transpiler::parse`) pass a value computed from runtime state, and the - /// resolver's `FsCache` forward-decl already pinned this shape. - /// PERF: re-monomorphize once both callers stabilize. - /// - /// `arena`: when - /// `!use_shared_buffer && arena.is_some()` the file body is read straight - /// into `arena` (`Contents::Arena`), so the bytes are bulk-freed by - /// `mi_heap_destroy` when the per-call `MimallocArena` (the per-job arena - /// from `RuntimeTranspilerStore` / `ParseTask`) drops — instead of round- - /// tripping through the worker thread's *default* mimalloc heap, which is - /// never destroyed and retains the fresh page for the process lifetime. - /// `None` keeps the global-heap `Contents::Owned(Vec)` path. - pub fn read_file_with_allocator( - &mut self, - _fs: &mut fs_mod::FileSystem, - path: &[u8], - dirname_fd: Fd, - use_shared_buffer: bool, - _file_handle: Option, - arena: Option<&bun_alloc::Arena>, - ) -> Result { - let rfs = &_fs.fs; - - // Single let-expression assigning `file_handle` on each branch, avoiding - // `mem::zeroed()` on a type that may have niche (NonZero) fields. - let mut _owned: Option = None; - let will_close: bool; - let fd: Fd = if let Some(f) = _file_handle { - bun_sys::File::borrow(&f) - .seek_to(0) - .map_err(bun_core::Error::from)?; - _owned = None; - will_close = false; - f - } else { - let opened = if feature_flags::STORE_FILE_DESCRIPTORS && dirname_fd.is_valid() { - match bun_sys::File::openat( - dirname_fd, - bun_paths::basename(path), - bun_sys::O::RDONLY, - 0, - ) { - Ok(f) => f, - Err(err) if err.get_errno() == bun_sys::E::ENOENT => { - let handle = bun_sys::open_file(path, bun_sys::OpenFlags::READ_ONLY) - .map_err(bun_core::Error::from)?; - bun_core::pretty_errorln!( - "Internal error: directory mismatch for directory \"{}\", fd {}. You don't need to do anything, but this indicates a bug.", - bstr::BStr::new(path), - dirname_fd, - ); - handle - } - Err(err) => return Err(err.into()), - } - } else { - bun_sys::open_file(path, bun_sys::OpenFlags::READ_ONLY) - .map_err(bun_core::Error::from)? - }; - let raw = opened.handle(); - will_close = rfs.need_to_close_files(); - _owned = Some(opened); - raw - }; - let file_handle = bun_sys::File::borrow(&fd); - - #[cfg(not(windows))] // skip on Windows because NTCreateFile will do it. - bun_core::scoped_log!( - fs, - "openat({}, {}) = {}", - dirname_fd, - bstr::BStr::new(path), - fd - ); - - // Borrowck: capture `stream` scalar before borrowing - // the shared buffer. - let stream = self.stream; - - let contents = match (use_shared_buffer, arena) { - // Read straight into the per-call arena so the source bytes are - // reclaimed by `mi_heap_destroy` instead of pinning a fresh page in - // the worker thread's default heap (one `mi_malloc` + `munmap` pair - // per transpiled module → one bump allocation in a wholesale-reset - // heap). - (false, Some(arena)) => { - match fs_mod::read_file_contents_in_arena(file_handle, path, arena) { - Ok((_, 0)) => Contents::Empty, - Ok((ptr, len)) => Contents::Arena { ptr, len }, - Err(err) => { - if cfg!(debug_assertions) { - Output::print_error(format_args!( - "{}: readFile error -- {}", - bstr::BStr::new(path), - bstr::BStr::new(err.name()), - )); - } - return Err(err); - } - } - } - _ => { - let shared = self.shared_buffer(); - match fs_mod::read_file_contents( - file_handle, - path, - use_shared_buffer, - shared, - stream, - ) - .map(Contents::from) - { - Ok(c) => c, - Err(err) => { - if cfg!(debug_assertions) { - Output::print_error(format_args!( - "{}: readFile error -- {}", - bstr::BStr::new(path), - bstr::BStr::new(err.name()), - )); - } - return Err(err); - } - } - } - }; - - let publish_fd = feature_flags::STORE_FILE_DESCRIPTORS && !will_close; - if publish_fd { - if let Some(f) = _owned.take() { - let _ = f.into_raw(); - } - } - Ok(Entry { - contents, - fd: if publish_fd { fd } else { Fd::INVALID }, - external_free_function: ExternalFreeFunction::NONE, - }) - } -} - pub struct Css {} pub struct CssEntry {} diff --git a/src/bundler/linker_context/generateChunksInParallel.rs b/src/bundler/linker_context/generateChunksInParallel.rs index f77ca1dcbb48..5322fccac22f 100644 --- a/src/bundler/linker_context/generateChunksInParallel.rs +++ b/src/bundler/linker_context/generateChunksInParallel.rs @@ -30,12 +30,12 @@ use crate::linker_context::prepare_css_asts_for_chunk::{ PrepareCssAstTask, prepare_css_asts_for_chunk, }; use crate::linker_context::static_route_visitor::StaticRouteVisitor; -use crate::linker_context::write_output_files_to_disk::write_output_files_to_disk; +use crate::linker_context::write_output_files_to_disk::{ + BYTECODE_EXTENSION, append_linked_sourcemap_url, generate_chunk_bytecode, + standalone_placeholder_output_file, write_output_files_to_disk, +}; use crate::linker_context_mod::{GenerateChunkCtx, PendingPartRange}; -/// Bytecode output file extension (also defined in `writeOutputFilesToDisk.rs`). -const BYTECODE_EXTENSION: &str = ".jsc"; - bun_core::declare_scope!(PartRanges, hidden); // `Chunk.final_rel_path` / `metafile_chunk_json` are owned @@ -677,27 +677,10 @@ pub fn generate_chunks_in_parallel( crate::chunk::Content::Html ) { - let _ = output_files.insert_for_chunk(options::OutputFile::init( - options::OutputFileInit { - data: options::OutputFileData::Buffer { - data: Box::default(), - }, - hash: None, - loader: chunks[chunk_index_in_chunks_list].content.loader(), - input_path: Box::default(), - display_size: 0, - output_kind: options::OutputKind::Chunk, - input_loader: Loader::Js, - output_path: Box::default(), - is_executable: false, - source_map_index: None, - bytecode_index: None, - module_info_index: None, - side: Some(options::Side::Client), - entry_point_index: None, - referenced_css_chunks: Box::default(), - bake_extra: BakeExtra::default(), - ..Default::default() + let _ = output_files.insert_for_chunk(standalone_placeholder_output_file( + chunks[chunk_index_in_chunks_list].content.loader(), + options::OutputFileData::Buffer { + data: Box::default(), }, )); continue; @@ -787,26 +770,11 @@ pub fn generate_chunks_in_parallel( source_map_final_rel_path.extend_from_slice(b".map"); if tag == SourceMapOption::Linked { - let [a, b]: [&[u8]; 2] = if public_path.len() > 0 { - cheap_prefix_normalizer(public_path, &source_map_final_rel_path) - } else { - [b"", path::basename(&source_map_final_rel_path)] - }; - - let source_map_start = b"//# sourceMappingURL="; - let total_len = code_result.buffer.len() - + source_map_start.len() - + a.len() - + b.len() - + b"\n".len(); - let mut buf: Vec = Vec::with_capacity(total_len); - buf.extend_from_slice(&code_result.buffer); - buf.extend_from_slice(source_map_start); - buf.extend_from_slice(a); - buf.extend_from_slice(b); - buf.push(b'\n'); - - code_result.buffer = buf.into_boxed_slice(); + append_linked_sourcemap_url( + &mut code_result.buffer, + public_path, + &source_map_final_rel_path, + ); } sourcemap_output_file = @@ -879,7 +847,6 @@ pub fn generate_chunks_in_parallel( if matches!(chunk.content, crate::chunk::Content::Javascript(_)) && loader.is_javascript_like() { - let mut fdpath = bun_paths::PathBuffer::uninit(); // For --compile builds, the bytecode URL must match the module name // that will be used at runtime. The module name is: // public_path + final_rel_path (e.g., "/$bunfs/root/app.js") @@ -903,33 +870,12 @@ pub fn generate_chunks_in_parallel( BYTECODE_EXTENSION )) }; - source_provider_url.ref_(); - // RAII: `defer source_provider_url.deref()` — `OwnedString::Drop` - // releases the ref bumped above on every exit path (incl. `break 'brk`). - let mut source_provider_url = - bun_core::OwnedString::new(source_provider_url); - - if let Some(bytecode) = crate::bundle_v2::dispatch::generate_cached_bytecode( + if let Some((bytecode, source_provider_url)) = generate_chunk_bytecode( c.options.output_format, &code_result.buffer, - &mut source_provider_url, + source_provider_url, ) { let source_provider_url_str = source_provider_url.to_utf8(); - debug!( - "Bytecode cache generated {}: {}", - bstr::BStr::new(source_provider_url_str.slice()), - bun_core::fmt::size( - bytecode.len(), - bun_core::fmt::SizeFormatterOptions { - space_between_number_and_unit: true - } - ) - ); - fdpath[..chunk.final_rel_path.len()] - .copy_from_slice(&chunk.final_rel_path); - fdpath[chunk.final_rel_path.len()..][..BYTECODE_EXTENSION.len()] - .copy_from_slice(BYTECODE_EXTENSION.as_bytes()); - let mut input_path_buf: Vec = Vec::new(); input_path_buf.extend_from_slice(&chunk.final_rel_path); input_path_buf.extend_from_slice(BYTECODE_EXTENSION.as_bytes()); diff --git a/src/bundler/linker_context/writeOutputFilesToDisk.rs b/src/bundler/linker_context/writeOutputFilesToDisk.rs index 9123b66ff471..0a30f7665efc 100644 --- a/src/bundler/linker_context/writeOutputFilesToDisk.rs +++ b/src/bundler/linker_context/writeOutputFilesToDisk.rs @@ -25,8 +25,78 @@ use bun_sys::{ write_file_with_path_buffer, }; -/// Bytecode output file extension (also defined in `generateChunksInParallel.rs`). -const BYTECODE_EXTENSION: &str = ".jsc"; +/// Bytecode output file extension (also used by `generateChunksInParallel.rs`). +pub(crate) const BYTECODE_EXTENSION: &str = ".jsc"; + +/// Append `//# sourceMappingURL=\n` to a chunk's code buffer for +/// `sourcemap: "linked"`, rebuilding the buffer at exact capacity. +pub(crate) fn append_linked_sourcemap_url( + buffer: &mut Box<[u8]>, + public_path: &[u8], + source_map_final_rel_path: &[u8], +) { + let [a, b]: [&[u8]; 2] = if !public_path.is_empty() { + cheap_prefix_normalizer(public_path, source_map_final_rel_path) + } else { + [b"", paths::basename(source_map_final_rel_path)] + }; + + let source_map_start = b"//# sourceMappingURL="; + let total_len = buffer.len() + source_map_start.len() + a.len() + b.len() + b"\n".len(); + let mut buf: Vec = Vec::with_capacity(total_len); + buf.extend_from_slice(buffer); + buf.extend_from_slice(source_map_start); + buf.extend_from_slice(a); + buf.extend_from_slice(b); + buf.push(b'\n'); + *buffer = buf.into_boxed_slice(); +} + +/// Generate the JSC bytecode cache for a chunk's code. Takes ownership of the +/// freshly created `source_provider_url` and returns it (still alive) next to +/// the bytecode so callers can read its UTF-8 form for the output path. +pub(crate) fn generate_chunk_bytecode( + format: options::Format, + code: &[u8], + source_provider_url: BunString, +) -> Option<(Box<[u8]>, bun_core::OwnedString)> { + source_provider_url.ref_(); + // RAII: `defer source_provider_url.deref()` — `OwnedString::Drop` releases + // the ref bumped above on every exit path. + let mut source_provider_url = bun_core::OwnedString::new(source_provider_url); + let bytecode = crate::bundle_v2::dispatch::generate_cached_bytecode( + format, + code, + &mut source_provider_url, + )?; + debug!( + "Bytecode cache generated {}: {}", + bstr::BStr::new(source_provider_url.to_utf8().slice()), + bun_core::fmt::size( + bytecode.len(), + bun_core::fmt::SizeFormatterOptions { + space_between_number_and_unit: true, + } + ), + ); + Some((bytecode, source_provider_url)) +} + +/// Placeholder output file inserted for non-HTML chunks in standalone mode to +/// keep chunk indices aligned. +pub(crate) fn standalone_placeholder_output_file( + loader: Loader, + data: OutputFileData, +) -> OutputFile { + OutputFile::init(OutputFileInit { + data, + loader, + input_loader: Loader::Js, + output_kind: options::OutputKind::Chunk, + side: Some(options::Side::Client), + ..Default::default() + }) +} pub fn write_output_files_to_disk( c: &mut LinkerContext, @@ -90,26 +160,10 @@ pub fn write_output_files_to_disk( // In standalone mode, only write HTML chunks to disk. // Insert placeholder output files for non-HTML chunks to keep indices aligned. if standalone_chunk_contents.is_some() && !matches!(chunk.content, Content::Html) { - let _ = output_files.insert_for_chunk(OutputFile::init(OutputFileInit { - data: OutputFileData::Saved(0), - hash: None, - loader: chunk.content.loader(), - input_path: Box::default(), - display_size: 0, - output_kind: options::OutputKind::Chunk, - input_loader: Loader::Js, - output_path: Box::default(), - is_executable: false, - source_map_index: None, - bytecode_index: None, - module_info_index: None, - side: Some(options::Side::Client), - entry_point_index: None, - referenced_css_chunks: Box::default(), - size: None, - source_index: IndexOptional::NONE, - bake_extra: BakeExtra::default(), - })); + let _ = output_files.insert_for_chunk(standalone_placeholder_output_file( + chunk.content.loader(), + OutputFileData::Saved(0), + )); continue; } @@ -222,25 +276,11 @@ pub fn write_output_files_to_disk( let source_map_final_rel_path = strings::concat(&[&chunk.final_rel_path, b".map"]); if tag == SourceMapOption::Linked { - let [a, b] = if !public_path.is_empty() { - cheap_prefix_normalizer(public_path, &source_map_final_rel_path) - } else { - [b"" as &[u8], paths::basename(&source_map_final_rel_path)] - }; - - let source_map_start = b"//# sourceMappingURL="; - let total_len = code_result.buffer.len() - + source_map_start.len() - + a.len() - + b.len() - + b"\n".len(); - let mut buf: Vec = Vec::with_capacity(total_len); - buf.extend_from_slice(&code_result.buffer); - buf.extend_from_slice(source_map_start); - buf.extend_from_slice(a); - buf.extend_from_slice(b); - buf.push(b'\n'); - code_result.buffer = buf.into_boxed_slice(); + append_linked_sourcemap_url( + &mut code_result.buffer, + public_path, + &source_map_final_rel_path, + ); } match bun_sys::File::write_file( @@ -324,26 +364,13 @@ pub fn write_output_files_to_disk( bstr::BStr::new(&chunk.final_rel_path), BYTECODE_EXTENSION, )); - source_provider_url.ref_(); - // `defer source_provider_url.deref()` handled by Drop on OwnedString. - let mut source_provider_url = bun_core::OwnedString::new(source_provider_url); - if let Some(bytecode) = crate::bundle_v2::dispatch::generate_cached_bytecode( + if let Some((bytecode, source_provider_url)) = generate_chunk_bytecode( c.options.output_format, &code_result.buffer, - &mut source_provider_url, + source_provider_url, ) { let source_provider_url_str = source_provider_url.to_utf8(); - debug!( - "Bytecode cache generated {}: {}", - bstr::BStr::new(source_provider_url_str.slice()), - bun_core::fmt::size( - bytecode.len(), - bun_core::fmt::SizeFormatterOptions { - space_between_number_and_unit: true, - } - ), - ); let frp: &[u8] = &chunk.final_rel_path; fdpath[..frp.len()].copy_from_slice(frp); fdpath[frp.len()..frp.len() + BYTECODE_EXTENSION.len()] diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index f251f910113f..5148ba207dd2 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -941,6 +941,27 @@ impl<'a> ParseResult<'a> { } } + #[inline] + fn with_ast( + ast: bun_ast::Ast<'a>, + source: &bun_ast::Source, + loader: options::Loader, + input_fd: Option, + source_contents_backing: resolver::cache::Contents, + ) -> Self { + ParseResult { + ast, + source: source.clone(), + loader, + input_fd, + already_bundled: AlreadyBundled::None, + pending_imports: Default::default(), + runtime_transpiler_cache: None, + empty: false, + source_contents_backing, + } + } + pub fn is_pending_import(&self, id: u32) -> bool { // AoS scan (see field comment); SoA column iteration restored // when `PendingResolution: MultiArrayElement` lands. @@ -1865,6 +1886,44 @@ impl<'a> Transpiler<'a> { // instead of being interleaved (post-LTO) with the hot JS/TS parse path. // --------------------------------------------------------------------------- +fn export_default_stmt(expr: bun_ast::Expr) -> bun_ast::Stmt { + bun_ast::Stmt::alloc( + bun_ast::S::ExportDefault { + value: bun_ast::StmtOrExpr::Expr(expr), + default_name: bun_ast::LocRef { + loc: bun_ast::Loc::default(), + ref_: Some(bun_ast::Ref::NONE), + }, + }, + bun_ast::Loc { start: 0 }, + ) +} + +/// Wrap `expr` as the sole `export default` statement of a single-part AST +/// and build the `ParseResult` around it. +#[cold] +fn export_default_parse_result<'a>( + expr: bun_ast::Expr, + source: &bun_ast::Source, + loader: options::Loader, + input_fd: Option, + source_backing: resolver::cache::Contents, + arena: &'a Arena, +) -> Option> { + let stmts = bun_ast::StoreSlice::new_mut(arena.alloc_slice_copy(&[export_default_stmt(expr)])); + let parts: Box<[bun_ast::Part]> = Box::new([bun_ast::Part { + stmts, + ..Default::default() + }]); + Some(ParseResult::with_ast( + bun_ast::Ast::from_parts(parts, arena), + source, + loader, + input_fd, + source_backing, + )) +} + #[cold] #[inline(never)] fn parse_data_loader<'a>( @@ -2046,16 +2105,7 @@ fn parse_data_loader<'a>( }, bun_ast::Loc { start: 0 }, ); - let stmt2 = bun_ast::Stmt::alloc( - bun_ast::S::ExportDefault { - value: bun_ast::StmtOrExpr::Expr(expr), - default_name: bun_ast::LocRef { - loc: bun_ast::Loc::default(), - ref_: Some(bun_ast::Ref::NONE), - }, - }, - bun_ast::Loc { start: 0 }, - ); + let stmt2 = export_default_stmt(expr); let stmts = bun_ast::StoreSlice::new_mut(arena.alloc_slice_copy(&[stmt0, stmt1, stmt2])); @@ -2066,39 +2116,20 @@ fn parse_data_loader<'a>( } } - { - let stmt = bun_ast::Stmt::alloc( - bun_ast::S::ExportDefault { - value: bun_ast::StmtOrExpr::Expr(expr), - default_name: bun_ast::LocRef { - loc: bun_ast::Loc::default(), - ref_: Some(bun_ast::Ref::NONE), - }, - }, - bun_ast::Loc { start: 0 }, - ); - - let stmts = bun_ast::StoreSlice::new_mut(arena.alloc_slice_copy(&[stmt])); - break 'parts Box::new([bun_ast::Part { - stmts, - ..Default::default() - }]); - } + // `symbols` is only populated by the non-empty-object branch above, + // which always `break 'parts`s; this fallthrough has no symbols. + return export_default_parse_result(expr, source, loader, input_fd, source_backing, arena); }; let mut ast = bun_ast::Ast::from_parts(parts, arena); ast.symbols = bun_alloc::vec_from_iter_in(symbols, arena); - return Some(ParseResult { + return Some(ParseResult::with_ast( ast, - source: source.clone(), + source, loader, input_fd, - already_bundled: AlreadyBundled::None, - pending_imports: Default::default(), - runtime_transpiler_cache: None, - empty: false, - source_contents_backing: source_backing, - }); + source_backing, + )); } #[cold] @@ -2114,33 +2145,7 @@ fn parse_text_loader<'a>( bun_ast::E::EString::init(&source.contents), bun_ast::Loc::EMPTY, ); - let stmt = bun_ast::Stmt::alloc( - bun_ast::S::ExportDefault { - value: bun_ast::StmtOrExpr::Expr(expr), - default_name: bun_ast::LocRef { - loc: bun_ast::Loc::default(), - ref_: Some(bun_ast::Ref::NONE), - }, - }, - bun_ast::Loc { start: 0 }, - ); - let stmts = bun_ast::StoreSlice::new_mut(arena.alloc_slice_copy(&[stmt])); - let parts: Box<[bun_ast::Part]> = Box::new([bun_ast::Part { - stmts, - ..Default::default() - }]); - - return Some(ParseResult { - ast: bun_ast::Ast::from_parts(parts, arena), - source: source.clone(), - loader, - input_fd, - already_bundled: AlreadyBundled::None, - pending_imports: Default::default(), - runtime_transpiler_cache: None, - empty: false, - source_contents_backing: source_backing, - }); + export_default_parse_result(expr, source, loader, input_fd, source_backing, arena) } #[cold] @@ -2172,33 +2177,7 @@ fn parse_md_loader<'a>( } }; let expr = bun_ast::Expr::init(bun_ast::E::EString::init(html), bun_ast::Loc::EMPTY); - let stmt = bun_ast::Stmt::alloc( - bun_ast::S::ExportDefault { - value: bun_ast::StmtOrExpr::Expr(expr), - default_name: bun_ast::LocRef { - loc: bun_ast::Loc::default(), - ref_: Some(bun_ast::Ref::NONE), - }, - }, - bun_ast::Loc { start: 0 }, - ); - let stmts = bun_ast::StoreSlice::new_mut(arena.alloc_slice_copy(&[stmt])); - let parts: Box<[bun_ast::Part]> = Box::new([bun_ast::Part { - stmts, - ..Default::default() - }]); - - return Some(ParseResult { - ast: bun_ast::Ast::from_parts(parts, arena), - source: source.clone(), - loader, - input_fd, - already_bundled: AlreadyBundled::None, - pending_imports: Default::default(), - runtime_transpiler_cache: None, - empty: false, - source_contents_backing: source_backing, - }); + export_default_parse_result(expr, source, loader, input_fd, source_backing, arena) } #[cold] @@ -2226,17 +2205,13 @@ fn parse_wasm_loader<'a>( return None; } - return Some(ParseResult { - ast: bun_ast::Ast::empty_in(arena), - source: source.clone(), + return Some(ParseResult::with_ast( + bun_ast::Ast::empty_in(arena), + source, loader, input_fd, - already_bundled: AlreadyBundled::None, - pending_imports: Default::default(), - runtime_transpiler_cache: None, - empty: false, - source_contents_backing: source_backing, - }); + source_backing, + )); } None } diff --git a/src/clap_macros/lib.rs b/src/clap_macros/lib.rs index 0969a83031f5..2ccad13dc5eb 100644 --- a/src/clap_macros/lib.rs +++ b/src/clap_macros/lib.rs @@ -265,91 +265,17 @@ 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)); + // without a TTY check) so it is a `const` byte literal in rodata; the ANSI + // form is derived lazily from `msg` by `bun_clap::pretty_help_desc` — it is + // rare (only `bun --help` on a colour TTY) and baking it in would roughly + // triple the help-string 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 f7a8c3b29489..2cffcc8141b6 100644 --- a/src/collections/array_hash_map.rs +++ b/src/collections/array_hash_map.rs @@ -567,18 +567,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 @@ -933,17 +921,6 @@ impl ArrayHashMap { } } - /// Mutable access to the entry at `index` (key + value). Returns `None` if - /// `index >= len`. Mirrors `indexmap::IndexMap::get_index_mut`. - pub fn get_index_mut(&mut self, index: usize) -> Option<(&mut K, &mut V)> { - if index >= self.keys.len() { - return None; - } - // `keys` and `values` are distinct struct fields; borrowck permits one - // `&mut` into each simultaneously. Bound proven above. - Some((&mut self.keys[index], &mut self.values[index])) - } - /// Remove the entry at `index` by swapping in the last /// entry. O(1); does not preserve insertion order. Returns the removed pair. pub fn swap_remove_at(&mut self, index: usize) -> (K, V) { @@ -1925,15 +1902,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 7fc46c48bd0a..c7f2a12e1f7a 100644 --- a/src/collections/pool.rs +++ b/src/collections/pool.rs @@ -66,35 +66,6 @@ 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 the singly-linked list from this node, until the final node is found. - /// This operation is O(N). - pub fn find_last(&mut self) -> *mut Node { - let mut it: *mut Node = std::ptr::from_mut::>(self); - loop { - let next = Node::next_of(it); - if next.is_null() { - return it; - } - 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 { diff --git a/src/css/compat.rs b/src/css/compat.rs index 2b556c4ddc72..825a727dbc23 100644 --- a/src/css/compat.rs +++ b/src/css/compat.rs @@ -1,4 +1,7 @@ -// This file is autogenerated by build-prefixes.js. DO NOT EDIT! +// Data derived from the output of build-prefixes.js (which generates the +// sibling compat.zig, ported from lightningcss). Unlike the generated code, +// which emits one match arm of version checks per feature, the data here is +// stored as a per-feature minimum-version table consumed by a generic check. use crate::targets::Browsers; @@ -223,5276 +226,300 @@ pub enum Feature { XResolutionUnit, } +/// Sentinel: the feature is not supported by this browser at any version. +const UNSUPPORTED: u32 = u32::MAX; + +/// Minimum browser version (encoded as `major << 16 | minor << 8 | patch`, +/// matching `Browsers`) required for native support of a feature. +struct MinVersions { + android: u32, + chrome: u32, + edge: u32, + firefox: u32, + ie: u32, + ios_saf: u32, + opera: u32, + safari: u32, + samsung: u32, +} + +/// Number of `Feature` variants; ties the table length to the enum so adding +/// a variant without a row fails to compile. +const FEATURE_COUNT: usize = Feature::XResolutionUnit as usize + 1; + +/// One entry per `Feature` variant, in declaration order (indexed by +/// `Feature as usize`). `None` means no browser supports the feature. +#[rustfmt::skip] +static MIN_VERSIONS: [Option; FEATURE_COUNT] = [ + Some(MinVersions { android: UNSUPPORTED, chrome: UNSUPPORTED, edge: UNSUPPORTED, firefox: 7733248, ie: UNSUPPORTED, ios_saf: 984064, opera: UNSUPPORTED, safari: 984064, samsung: UNSUPPORTED }), // AbsFunction + Some(MinVersions { android: UNSUPPORTED, chrome: UNSUPPORTED, edge: UNSUPPORTED, firefox: 6750208, ie: UNSUPPORTED, ios_saf: 1049856, opera: UNSUPPORTED, safari: 1049856, samsung: UNSUPPORTED }), // AccentSystemColor + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // AfarListStyleType + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // AmharicAbegedeListStyleType + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // AmharicListStyleType + Some(MinVersions { android: 8192000, chrome: 8192000, edge: 8192000, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: UNSUPPORTED, opera: 5439488, safari: UNSUPPORTED, samsung: UNSUPPORTED }), // AnchorSizeSize + Some(MinVersions { android: 7536640, chrome: 7536640, edge: 7536640, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: UNSUPPORTED, opera: 5046272, safari: UNSUPPORTED, samsung: 1507328 }), // AnimationTimelineShorthand + Some(MinVersions { android: 8585216, chrome: 4259840, edge: 5177344, firefox: 3276800, ie: UNSUPPORTED, ios_saf: 589824, opera: 3407872, safari: 589824, samsung: 590336 }), // AnyLink + Some(MinVersions { android: 2424832, chrome: 1179648, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 327680, opera: 917504, safari: 327680, samsung: 65536 }), // AnyPseudo + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // ArabicIndicListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 524288, ios_saf: 65536, opera: 655616, safari: 65536, samsung: 65536 }), // ArmenianListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 327680, opera: 917504, safari: 327936, samsung: 65536 }), // AsterisksListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 720896, ios_saf: 65536, opera: 917504, safari: 65536, samsung: 65536 }), // AutoSize + Some(MinVersions { android: 8585216, chrome: 7208960, edge: 7208960, firefox: 5636096, ie: UNSUPPORTED, ios_saf: 983040, opera: 6291456, safari: 983040, samsung: 1376256 }), // Autofill + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // BengaliListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // BinaryListStyleType + Some(MinVersions { android: 263168, chrome: 1966080, edge: 786432, firefox: 983040, ie: 720896, ios_saf: 590592, opera: 1179648, safari: 590080, samsung: 131072 }), // BorderImageRepeatRound + Some(MinVersions { android: 3670016, chrome: 3670016, edge: 786432, firefox: 3276800, ie: 720896, ios_saf: 590592, opera: 2818048, safari: 590080, samsung: 393216 }), // BorderImageRepeatSpace + Some(MinVersions { android: 8585216, chrome: 1703936, edge: 786432, firefox: 1048576, ie: UNSUPPORTED, ios_saf: 458752, opera: 983040, safari: 393472, samsung: 262144 }), // CalcFunction + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 2162688, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // CambodianListStyleType + Some(MinVersions { android: 7733248, chrome: 7733248, edge: 7733248, firefox: 6356992, ie: UNSUPPORTED, ios_saf: 1114624, opera: 5177344, safari: 1114624, samsung: 1638400 }), // CapUnit + Some(MinVersions { android: 8585216, chrome: 3211264, edge: 5177344, firefox: 3080192, ie: UNSUPPORTED, ios_saf: 589824, opera: 2359296, safari: 589824, samsung: 327680 }), // CaseInsensitive + Some(MinVersions { android: 263168, chrome: 1769472, edge: 786432, firefox: 262144, ie: 589824, ios_saf: 458752, opera: 983040, safari: 458752, samsung: 66816 }), // ChUnit + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 262144, ios_saf: 65536, opera: 655616, safari: 65536, samsung: 65536 }), // CircleListStyleType + Some(MinVersions { android: 5963776, chrome: 5963776, edge: 5963776, firefox: 1835008, ie: UNSUPPORTED, ios_saf: 983040, opera: 4194304, safari: 983040, samsung: 1048576 }), // CjkDecimalListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // CjkEarthlyBranchListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // CjkHeavenlyStemListStyleType + Some(MinVersions { android: 5177344, chrome: 5177344, edge: 5177344, firefox: 5177344, ie: UNSUPPORTED, ios_saf: 852992, opera: 3735552, safari: 852224, samsung: 786432 }), // ClampFunction + Some(MinVersions { android: 7274496, chrome: 7274496, edge: 7274496, firefox: 7405568, ie: UNSUPPORTED, ios_saf: 656128, opera: 4915200, safari: 655616, samsung: 1441792 }), // ColorFunction + Some(MinVersions { android: 4521984, chrome: 4521984, edge: 5177344, firefox: 5439488, ie: UNSUPPORTED, ios_saf: 786944, opera: 3145728, safari: 786688, samsung: 655360 }), // ConicGradient + Some(MinVersions { android: 6881280, chrome: 6881280, edge: 6881280, firefox: 7208960, ie: UNSUPPORTED, ios_saf: 1048576, opera: 4718592, safari: 1048576, samsung: 1310720 }), // ContainerQueryLengthUnits + Some(MinVersions { android: 263168, chrome: 1703936, edge: 5177344, firefox: 3604480, ie: UNSUPPORTED, ios_saf: 458752, opera: 917504, safari: 458752, samsung: 66816 }), // Cue + Some(MinVersions { android: 263168, chrome: 1703936, edge: 5177344, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 458752, opera: 917504, safari: 458752, samsung: 66816 }), // CueFunction + None, // CustomMediaQueries + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 524288, ios_saf: 65536, opera: 655616, safari: 65536, samsung: 65536 }), // DecimalLeadingZeroListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 262144, ios_saf: 65536, opera: 655616, safari: 65536, samsung: 65536 }), // DecimalListStyleType + Some(MinVersions { android: 8585216, chrome: 3342336, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 656128, opera: 2490368, safari: 655616, samsung: 327680 }), // DefaultPseudo + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // DevanagariListStyleType + Some(MinVersions { android: 8585216, chrome: 2424832, edge: 5177344, firefox: 6422528, ie: UNSUPPORTED, ios_saf: 984064, opera: 1572864, safari: 984064, samsung: 262144 }), // Dialog + Some(MinVersions { android: 8585216, chrome: 7864320, edge: 7864320, firefox: 3211264, ie: UNSUPPORTED, ios_saf: 1049600, opera: 6946816, safari: 1049600, samsung: 1638400 }), // DirSelector + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 262144, ios_saf: 65536, opera: 655616, safari: 65536, samsung: 65536 }), // DiscListStyleType + Some(MinVersions { android: 5832704, chrome: 5832704, edge: 5832704, firefox: 2162688, ie: UNSUPPORTED, ios_saf: 983040, opera: 4128768, safari: 983040, samsung: 983040 }), // DisclosureClosedListStyleType + Some(MinVersions { android: 5832704, chrome: 5832704, edge: 5832704, firefox: 2162688, ie: UNSUPPORTED, ios_saf: 983040, opera: 4128768, safari: 983040, samsung: 983040 }), // DisclosureOpenListStyleType + Some(MinVersions { android: 4653056, chrome: 4653056, edge: 5177344, firefox: 4194304, ie: UNSUPPORTED, ios_saf: 786944, opera: 3276800, safari: 786688, samsung: 655360 }), // DoublePositionGradients + Some(MinVersions { android: 65536, chrome: 1179648, edge: 786432, firefox: 262144, ie: 196608, ios_saf: 65536, opera: 655616, safari: 65536, samsung: 65536 }), // EmUnit + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // EthiopicAbegedeAmEtListStyleType + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // EthiopicAbegedeGezListStyleType + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // EthiopicAbegedeListStyleType + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // EthiopicAbegedeTiErListStyleType + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // EthiopicAbegedeTiEtListStyleType + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // EthiopicHalehameAaErListStyleType + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // EthiopicHalehameAaEtListStyleType + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // EthiopicHalehameAmEtListStyleType + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // EthiopicHalehameGezListStyleType + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // EthiopicHalehameOmEtListStyleType + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // EthiopicHalehameSidEtListStyleType + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // EthiopicHalehameSoEtListStyleType + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // EthiopicHalehameTigListStyleType + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // EthiopicListStyleType + Some(MinVersions { android: 5963776, chrome: 5963776, edge: 5963776, firefox: 262144, ie: UNSUPPORTED, ios_saf: 983040, opera: 4194304, safari: 983040, samsung: 1048576 }), // EthiopicNumericListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 262144, ios_saf: 65536, opera: 655616, safari: 65536, samsung: 65536 }), // ExUnit + Some(MinVersions { android: UNSUPPORTED, chrome: UNSUPPORTED, edge: UNSUPPORTED, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 852992, opera: UNSUPPORTED, safari: 852224, samsung: UNSUPPORTED }), // ExtendedSystemFonts + Some(MinVersions { android: 196608, chrome: 589824, edge: 786432, firefox: 197888, ie: 589824, ios_saf: 327680, opera: 722432, safari: 327936, samsung: 262144 }), // FirstLetter + Some(MinVersions { android: 131328, chrome: 262144, edge: 786432, firefox: 131072, ie: 589824, ios_saf: 197120, opera: 589824, safari: 196864, samsung: 262144 }), // FirstLine + None, // FitContentFunctionSize + Some(MinVersions { android: 263168, chrome: 1638400, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 458752, opera: 917504, safari: 458752, samsung: 66816 }), // FitContentSize + Some(MinVersions { android: 8585216, chrome: 5636096, edge: 5636096, firefox: 5570560, ie: UNSUPPORTED, ios_saf: 984064, opera: 4718592, safari: 984064, samsung: 917504 }), // FocusVisible + Some(MinVersions { android: 8585216, chrome: 3932160, edge: 5177344, firefox: 3407872, ie: UNSUPPORTED, ios_saf: 656128, opera: 3080192, safari: 655616, samsung: 524800 }), // FocusWithin + Some(MinVersions { android: 8585216, chrome: 3670016, edge: 5177344, firefox: 6029312, ie: UNSUPPORTED, ios_saf: 720896, opera: 2818048, safari: 720896, samsung: 393728 }), // FontFamilySystemUi + Some(MinVersions { android: 2752512, chrome: 2752512, edge: 786432, firefox: 2031616, ie: 589824, ios_saf: 458752, opera: 1835008, safari: 458752, samsung: 262144 }), // FontSizeRem + Some(MinVersions { android: 5177344, chrome: 5177344, edge: 5177344, firefox: 5177344, ie: UNSUPPORTED, ios_saf: 1049600, opera: 3735552, safari: 1049600, samsung: 786432 }), // FontSizeXXXLarge + Some(MinVersions { android: 4063232, chrome: 4063232, edge: 1179648, firefox: 3997696, ie: UNSUPPORTED, ios_saf: 721664, opera: 3014656, safari: 721152, samsung: 524288 }), // FontStretchPercentage + Some(MinVersions { android: 4063232, chrome: 4063232, edge: 5177344, firefox: 3997696, ie: UNSUPPORTED, ios_saf: 721664, opera: 3014656, safari: 721152, samsung: 524288 }), // FontStyleObliqueAngle + Some(MinVersions { android: 4063232, chrome: 4063232, edge: 1114112, firefox: 3997696, ie: UNSUPPORTED, ios_saf: 720896, opera: 3014656, safari: 720896, samsung: 524288 }), // FontWeightNumber + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 327680, opera: 917504, safari: 327936, samsung: 65536 }), // FootnotesListStyleType + Some(MinVersions { android: 263171, chrome: 655360, edge: 786432, firefox: 262144, ie: 655360, ios_saf: 656128, opera: 655360, safari: 655616, samsung: 262144 }), // FormValidation + Some(MinVersions { android: UNSUPPORTED, chrome: 4653056, edge: 5177344, firefox: 4194304, ie: UNSUPPORTED, ios_saf: UNSUPPORTED, opera: 786688, safari: 1049600, samsung: 655616 }), // Fullscreen + Some(MinVersions { android: 131328, chrome: 262144, edge: 786432, firefox: 131072, ie: 589824, ios_saf: 197120, opera: 589824, safari: 196864, samsung: 262144 }), // Gencontent + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 524288, ios_saf: 65536, opera: 655616, safari: 65536, samsung: 65536 }), // GeorgianListStyleType + Some(MinVersions { android: 2621440, chrome: 2621440, edge: 5177344, firefox: 2359296, ie: UNSUPPORTED, ios_saf: 458752, opera: 1769472, safari: 458752, samsung: 262144 }), // GradientInterpolationHints + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // GujaratiListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // GurmukhiListStyleType + Some(MinVersions { android: 8585216, chrome: 6881280, edge: 6881280, firefox: 7929856, ie: UNSUPPORTED, ios_saf: 984064, opera: 5963776, safari: 984064, samsung: 1310720 }), // HasSelector + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 720896, ios_saf: 65536, opera: 917504, safari: 65536, samsung: 65536 }), // HebrewListStyleType + Some(MinVersions { android: 8585216, chrome: 4063232, edge: 5177344, firefox: 3211264, ie: UNSUPPORTED, ios_saf: 655360, opera: 3407872, safari: 655360, samsung: 524800 }), // HexAlphaColors + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 720896, ios_saf: 65536, opera: 917504, safari: 65536, samsung: 65536 }), // HiraganaIrohaListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 720896, ios_saf: 65536, opera: 917504, safari: 65536, samsung: 65536 }), // HiraganaListStyleType + Some(MinVersions { android: 7864320, chrome: 7864320, edge: 7864320, firefox: 7733248, ie: UNSUPPORTED, ios_saf: 984064, opera: 5242880, safari: 984064, samsung: 1638400 }), // HypotFunction + Some(MinVersions { android: 6946816, chrome: 6946816, edge: 6946816, firefox: 6356992, ie: UNSUPPORTED, ios_saf: 984064, opera: 4718592, safari: 984064, samsung: 1310720 }), // IcUnit + Some(MinVersions { android: 263168, chrome: 1638400, edge: 5177344, firefox: 5767168, ie: UNSUPPORTED, ios_saf: 393216, opera: 917504, safari: 393216, samsung: 66816 }), // ImageSet + Some(MinVersions { android: 8585216, chrome: 3473408, edge: 5177344, firefox: 3276800, ie: UNSUPPORTED, ios_saf: 656128, opera: 2621440, safari: 655616, samsung: 327680 }), // InOutOfRange + Some(MinVersions { android: 8585216, chrome: 2555904, edge: 5177344, firefox: 3342336, ie: UNSUPPORTED, ios_saf: 656128, opera: 1703936, safari: 655616, samsung: 262144 }), // IndeterminatePseudo + Some(MinVersions { android: 263168, chrome: 1703936, edge: 786432, firefox: 1048576, ie: 720896, ios_saf: 458752, opera: 917504, safari: 458752, samsung: 66816 }), // IsAnimatableSize + Some(MinVersions { android: 8585216, chrome: 5767168, edge: 5767168, firefox: 5111808, ie: UNSUPPORTED, ios_saf: 917504, opera: 4915200, safari: 917504, samsung: 983040 }), // IsSelector + Some(MinVersions { android: 5963776, chrome: 5963776, edge: 5963776, firefox: 262144, ie: UNSUPPORTED, ios_saf: 983040, opera: 4194304, safari: 983040, samsung: 1048576 }), // JapaneseFormalListStyleType + Some(MinVersions { android: 5963776, chrome: 5963776, edge: 5963776, firefox: 262144, ie: UNSUPPORTED, ios_saf: 983040, opera: 4194304, safari: 983040, samsung: 1048576 }), // JapaneseInformalListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // KannadaListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 720896, ios_saf: 65536, opera: 917504, safari: 65536, samsung: 65536 }), // KatakanaIrohaListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 720896, ios_saf: 65536, opera: 917504, safari: 65536, samsung: 65536 }), // KatakanaListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // KhmerListStyleType + Some(MinVersions { android: 2949120, chrome: 2949120, edge: 5177344, firefox: 1835008, ie: UNSUPPORTED, ios_saf: 983040, opera: 2097152, safari: 983040, samsung: 327680 }), // KoreanHangulFormalListStyleType + Some(MinVersions { android: 2949120, chrome: 2949120, edge: 5177344, firefox: 1835008, ie: UNSUPPORTED, ios_saf: 983040, opera: 2097152, safari: 983040, samsung: 327680 }), // KoreanHanjaFormalListStyleType + Some(MinVersions { android: 2949120, chrome: 2949120, edge: 5177344, firefox: 1835008, ie: UNSUPPORTED, ios_saf: 983040, opera: 2097152, safari: 983040, samsung: 327680 }), // KoreanHanjaInformalListStyleType + Some(MinVersions { android: 7274496, chrome: 7274496, edge: 7274496, firefox: 7405568, ie: UNSUPPORTED, ios_saf: 983040, opera: 4915200, safari: 983040, samsung: 1441792 }), // LabColors + Some(MinVersions { android: UNSUPPORTED, chrome: UNSUPPORTED, edge: UNSUPPORTED, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 656128, opera: UNSUPPORTED, safari: 655616, samsung: UNSUPPORTED }), // LangSelectorList + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // LaoListStyleType + Some(MinVersions { android: 7143424, chrome: 7143424, edge: 7143424, firefox: 7864320, ie: UNSUPPORTED, ios_saf: 1049600, opera: 4849664, safari: 1049600, samsung: 1376256 }), // LhUnit + Some(MinVersions { android: 8060928, chrome: 8060928, edge: 8060928, firefox: 7864320, ie: UNSUPPORTED, ios_saf: 1115392, opera: 5373952, safari: 1115392, samsung: UNSUPPORTED }), // LightDark + Some(MinVersions { android: 2424832, chrome: 1179648, edge: 786432, firefox: 262144, ie: 655360, ios_saf: 327680, opera: 720896, safari: 327936, samsung: 65536 }), // LinearGradient + Some(MinVersions { android: 5832704, chrome: 5832704, edge: 5832704, firefox: 4325376, ie: UNSUPPORTED, ios_saf: 983040, opera: 4128768, safari: 983040, samsung: 983040 }), // LogicalBorderRadius + Some(MinVersions { android: 5701632, chrome: 5701632, edge: 5701632, firefox: 4325376, ie: UNSUPPORTED, ios_saf: 918784, opera: 4063232, safari: 917760, samsung: 917504 }), // LogicalBorderShorthand + Some(MinVersions { android: 4521984, chrome: 4521984, edge: 5177344, firefox: 2686976, ie: UNSUPPORTED, ios_saf: 786944, opera: 3145728, safari: 786688, samsung: 655360 }), // LogicalBorders + Some(MinVersions { android: 5701632, chrome: 5701632, edge: 5701632, firefox: 4128768, ie: UNSUPPORTED, ios_saf: 918784, opera: 4063232, safari: 917760, samsung: 917504 }), // LogicalInset + Some(MinVersions { android: 5701632, chrome: 4521984, edge: 5177344, firefox: 2686976, ie: UNSUPPORTED, ios_saf: 786944, opera: 3145728, safari: 786688, samsung: 655360 }), // LogicalMargin + Some(MinVersions { android: 5701632, chrome: 5701632, edge: 5701632, firefox: 4325376, ie: UNSUPPORTED, ios_saf: 918784, opera: 4063232, safari: 917760, samsung: 917504 }), // LogicalMarginShorthand + Some(MinVersions { android: 5701632, chrome: 4521984, edge: 5177344, firefox: 2686976, ie: UNSUPPORTED, ios_saf: 786944, opera: 3145728, safari: 786688, samsung: 655360 }), // LogicalPadding + Some(MinVersions { android: 5701632, chrome: 5701632, edge: 5701632, firefox: 4325376, ie: UNSUPPORTED, ios_saf: 918784, opera: 4063232, safari: 917760, samsung: 917504 }), // LogicalPaddingShorthand + Some(MinVersions { android: 3735552, chrome: 3735552, edge: 5177344, firefox: 2686976, ie: UNSUPPORTED, ios_saf: 786944, opera: 2818048, safari: 786688, samsung: 327680 }), // LogicalSize + Some(MinVersions { android: 2424832, chrome: 1179648, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 131072, opera: 917504, safari: 196864, samsung: 65536 }), // LogicalTextAlign + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 524288, ios_saf: 65536, opera: 655616, safari: 65536, samsung: 65536 }), // LowerAlphaListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 2162688, ie: UNSUPPORTED, ios_saf: 327680, opera: 917504, safari: 327936, samsung: 65536 }), // LowerArmenianListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 524288, ios_saf: 65536, opera: 655616, safari: 65536, samsung: 65536 }), // LowerGreekListStyleType + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // LowerHexadecimalListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 524288, ios_saf: 65536, opera: 655616, safari: 65536, samsung: 327680 }), // LowerLatinListStyleType + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // LowerNorwegianListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 524288, ios_saf: 65536, opera: 655616, safari: 65536, samsung: 65536 }), // LowerRomanListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // MalayalamListStyleType + Some(MinVersions { android: 8585216, chrome: 5636096, edge: 5636096, firefox: 4456448, ie: UNSUPPORTED, ios_saf: UNSUPPORTED, opera: 4718592, safari: UNSUPPORTED, samsung: 917504 }), // MarkerPseudo + Some(MinVersions { android: 263168, chrome: 1638400, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 720896, opera: 2818048, safari: 720896, samsung: 66816 }), // MaxContentSize + Some(MinVersions { android: 5177344, chrome: 5177344, edge: 5177344, firefox: 5177344, ie: UNSUPPORTED, ios_saf: 721664, opera: 3735552, safari: 721152, samsung: 786432 }), // MaxFunction + Some(MinVersions { android: 6815744, chrome: 6815744, edge: 6815744, firefox: 6684672, ie: UNSUPPORTED, ios_saf: 1049600, opera: 4653056, safari: 1049600, samsung: 1310720 }), // MediaIntervalSyntax + Some(MinVersions { android: 6815744, chrome: 6815744, edge: 6815744, firefox: 4128768, ie: UNSUPPORTED, ios_saf: 1049600, opera: 4653056, safari: 1049600, samsung: 1310720 }), // MediaRangeSyntax + Some(MinVersions { android: 3014656, chrome: 3014656, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 720896, opera: 2162688, safari: 720896, samsung: 327680 }), // MinContentSize + Some(MinVersions { android: 5177344, chrome: 5177344, edge: 5177344, firefox: 5177344, ie: UNSUPPORTED, ios_saf: 721664, opera: 3735552, safari: 721152, samsung: 786432 }), // MinFunction + Some(MinVersions { android: 8192000, chrome: 8192000, edge: 8192000, firefox: 7733248, ie: UNSUPPORTED, ios_saf: 984064, opera: 5439488, safari: 984064, samsung: UNSUPPORTED }), // ModFunction + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 2162688, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // MongolianListStyleType + Some(MinVersions { android: UNSUPPORTED, chrome: UNSUPPORTED, edge: UNSUPPORTED, firefox: 262144, ie: UNSUPPORTED, ios_saf: UNSUPPORTED, opera: UNSUPPORTED, safari: UNSUPPORTED, samsung: UNSUPPORTED }), // MozAvailableSize + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // MyanmarListStyleType + Some(MinVersions { android: 131328, chrome: 262144, edge: 786432, firefox: 131072, ie: 589824, ios_saf: 262656, opera: 589824, safari: 262144, samsung: 262144 }), // Namespaces + Some(MinVersions { android: 8585216, chrome: 7864320, edge: 7864320, firefox: 7667712, ie: UNSUPPORTED, ios_saf: 1114624, opera: 6946816, safari: 1114624, samsung: UNSUPPORTED }), // Nesting + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 5177344, ie: 720896, ios_saf: 65536, opera: 917504, safari: 65536, samsung: 65536 }), // NoneListStyleType + Some(MinVersions { android: 8585216, chrome: 5767168, edge: 5767168, firefox: 5505024, ie: UNSUPPORTED, ios_saf: 589824, opera: 4915200, safari: 589824, samsung: 983040 }), // NotSelectorList + Some(MinVersions { android: 7274496, chrome: 7274496, edge: 7274496, firefox: 7405568, ie: UNSUPPORTED, ios_saf: 589824, opera: 4915200, safari: 589824, samsung: 1441792 }), // NthChildOf + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // OctalListStyleType + Some(MinVersions { android: 7274496, chrome: 7274496, edge: 7274496, firefox: 7405568, ie: UNSUPPORTED, ios_saf: 984064, opera: 4915200, safari: 984064, samsung: 1441792 }), // OklabColors + Some(MinVersions { android: 131840, chrome: 983040, edge: 786432, firefox: 262144, ie: 655360, ios_saf: 327680, opera: 983040, safari: 327680, samsung: 262144 }), // OptionalPseudo + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // OriyaListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // OromoListStyleType + Some(MinVersions { android: 4456448, chrome: 4456448, edge: 5177344, firefox: 3997696, ie: UNSUPPORTED, ios_saf: 852992, opera: 3145728, safari: 852224, samsung: 655360 }), // OverflowShorthand + Some(MinVersions { android: UNSUPPORTED, chrome: UNSUPPORTED, edge: UNSUPPORTED, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 656128, opera: UNSUPPORTED, safari: 655616, samsung: UNSUPPORTED }), // P3Colors + Some(MinVersions { android: 4784128, chrome: 4784128, edge: 5177344, firefox: 5177344, ie: UNSUPPORTED, ios_saf: 852992, opera: 3407872, safari: 852224, samsung: 720896 }), // PartPseudo + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // PersianListStyleType + Some(MinVersions { android: 3866624, chrome: 3866624, edge: 5177344, firefox: 2949120, ie: UNSUPPORTED, ios_saf: 589824, opera: 2818048, safari: 589824, samsung: 458752 }), // PlaceContent + Some(MinVersions { android: 3866624, chrome: 3866624, edge: 5177344, firefox: 2949120, ie: UNSUPPORTED, ios_saf: 720896, opera: 2818048, safari: 720896, samsung: 458752 }), // PlaceItems + Some(MinVersions { android: 3866624, chrome: 3866624, edge: 5177344, firefox: 2949120, ie: UNSUPPORTED, ios_saf: 720896, opera: 2818048, safari: 720896, samsung: 458752 }), // PlaceSelf + Some(MinVersions { android: 8585216, chrome: 3735552, edge: 5177344, firefox: 3342336, ie: UNSUPPORTED, ios_saf: 656128, opera: 2883584, safari: 655616, samsung: 459264 }), // Placeholder + Some(MinVersions { android: 8585216, chrome: 3080192, edge: 5177344, firefox: 3342336, ie: UNSUPPORTED, ios_saf: 589824, opera: 2228224, safari: 589824, samsung: 327680 }), // PlaceholderShown + Some(MinVersions { android: 4128768, chrome: 4128768, edge: 5177344, firefox: 3211264, ie: UNSUPPORTED, ios_saf: 852992, opera: 3014656, safari: 852224, samsung: 524288 }), // QUnit + Some(MinVersions { android: 2424832, chrome: 1179648, edge: 786432, firefox: 262144, ie: 655360, ios_saf: 327680, opera: 786432, safari: 327936, samsung: 65536 }), // RadialGradient + Some(MinVersions { android: 7733248, chrome: 7733248, edge: 7733248, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 1114624, opera: 5177344, safari: 1114624, samsung: 1638400 }), // RcapUnit + Some(MinVersions { android: 7274496, chrome: 7274496, edge: 7274496, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 1114624, opera: 4915200, safari: 1114624, samsung: 1441792 }), // RchUnit + Some(MinVersions { android: 8585216, chrome: 2359296, edge: 851968, firefox: 5111808, ie: UNSUPPORTED, ios_saf: 589824, opera: 1507328, safari: 589824, samsung: 262144 }), // ReadOnlyWrite + Some(MinVersions { android: 8192000, chrome: 8192000, edge: 8192000, firefox: 7733248, ie: UNSUPPORTED, ios_saf: 984064, opera: 5439488, safari: 984064, samsung: UNSUPPORTED }), // RemFunction + Some(MinVersions { android: 131072, chrome: 1179648, edge: 786432, firefox: 262144, ie: 589824, ios_saf: 262144, opera: 786432, safari: 327680, samsung: 65536 }), // RemUnit + Some(MinVersions { android: 4521984, chrome: 4521984, edge: 5177344, firefox: 5439488, ie: UNSUPPORTED, ios_saf: 786944, opera: 3145728, safari: 786688, samsung: 655360 }), // RepeatingConicGradient + Some(MinVersions { android: 2424832, chrome: 1179648, edge: 786432, firefox: 262144, ie: 655360, ios_saf: 327680, opera: 720896, safari: 327936, samsung: 65536 }), // RepeatingLinearGradient + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 655360, ie: 655360, ios_saf: 327680, opera: 786432, safari: 327936, samsung: 65536 }), // RepeatingRadialGradient + Some(MinVersions { android: 7274496, chrome: 7274496, edge: 7274496, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 1114624, opera: 4915200, safari: 1114624, samsung: 1441792 }), // RexUnit + Some(MinVersions { android: 7274496, chrome: 7274496, edge: 7274496, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 1114624, opera: 4915200, safari: 1114624, samsung: 1441792 }), // RicUnit + Some(MinVersions { android: 7274496, chrome: 7274496, edge: 7274496, firefox: 7864320, ie: UNSUPPORTED, ios_saf: 1049600, opera: 4915200, safari: 1049600, samsung: 1441792 }), // RlhUnit + Some(MinVersions { android: 8192000, chrome: 8192000, edge: 8192000, firefox: 7733248, ie: UNSUPPORTED, ios_saf: 984064, opera: 5439488, safari: 984064, samsung: UNSUPPORTED }), // RoundFunction + Some(MinVersions { android: 263168, chrome: 262144, edge: 786432, firefox: 4063232, ie: 589824, ios_saf: UNSUPPORTED, opera: 591104, safari: 196864, samsung: 262144 }), // Selection + Some(MinVersions { android: 131328, chrome: 262144, edge: 786432, firefox: 131072, ie: 458752, ios_saf: 197120, opera: 589824, safari: 196864, samsung: 262144 }), // Selectors2 + Some(MinVersions { android: 131328, chrome: 262144, edge: 786432, firefox: 197888, ie: 589824, ios_saf: 197120, opera: 591104, safari: 197120, samsung: 262144 }), // Selectors3 + Some(MinVersions { android: 8585216, chrome: 3473408, edge: 5177344, firefox: 4128768, ie: UNSUPPORTED, ios_saf: 720896, opera: 2621440, safari: 655360, samsung: 393728 }), // Shadowdomv1 + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // SidamaListStyleType + Some(MinVersions { android: UNSUPPORTED, chrome: UNSUPPORTED, edge: UNSUPPORTED, firefox: 7733248, ie: UNSUPPORTED, ios_saf: 984064, opera: UNSUPPORTED, safari: 984064, samsung: UNSUPPORTED }), // SignFunction + Some(MinVersions { android: 2949120, chrome: 2949120, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 983040, opera: 2097152, safari: 983040, samsung: 327680 }), // SimpChineseFormalListStyleType + Some(MinVersions { android: 2949120, chrome: 2949120, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 983040, opera: 2097152, safari: 983040, samsung: 327680 }), // SimpChineseInformalListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // SomaliListStyleType + Some(MinVersions { android: 4259840, chrome: 4259840, edge: 5177344, firefox: 3407872, ie: UNSUPPORTED, ios_saf: 786944, opera: 3080192, safari: 786688, samsung: 589824 }), // SpaceSeparatedColorNotation + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 262144, ios_saf: 65536, opera: 655616, safari: 65536, samsung: 65536 }), // SquareListStyleType + None, // StretchSize + Some(MinVersions { android: 5177344, chrome: 5177344, edge: 5177344, firefox: 2555904, ie: UNSUPPORTED, ios_saf: 918784, opera: 3735552, safari: 917760, samsung: 786432 }), // StringListStyleType + Some(MinVersions { android: UNSUPPORTED, chrome: UNSUPPORTED, edge: UNSUPPORTED, firefox: 2293760, ie: UNSUPPORTED, ios_saf: UNSUPPORTED, opera: UNSUPPORTED, safari: UNSUPPORTED, samsung: UNSUPPORTED }), // SymbolsListStyleType + Some(MinVersions { android: 5963776, chrome: 5963776, edge: 5963776, firefox: 262144, ie: UNSUPPORTED, ios_saf: 983040, opera: 4194304, safari: 983040, samsung: 1048576 }), // TamilListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // TeluguListStyleType + Some(MinVersions { android: 5701632, chrome: 5701632, edge: 5701632, firefox: 5177344, ie: UNSUPPORTED, ios_saf: 1115136, opera: 4063232, safari: 1115136, samsung: 917504 }), // TextDecorationThicknessPercent + Some(MinVersions { android: 5701632, chrome: 5701632, edge: 5701632, firefox: 5177344, ie: UNSUPPORTED, ios_saf: UNSUPPORTED, opera: 4063232, safari: UNSUPPORTED, samsung: 917504 }), // TextDecorationThicknessShorthand + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // ThaiListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 2162688, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // TibetanListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // TigreListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // TigrinyaErAbegedeListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // TigrinyaErListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // TigrinyaEtAbegedeListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // TigrinyaEtListStyleType + Some(MinVersions { android: 2949120, chrome: 2949120, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 983040, opera: 2097152, safari: 983040, samsung: 327680 }), // TradChineseFormalListStyleType + Some(MinVersions { android: 2949120, chrome: 2949120, edge: 5177344, firefox: 262144, ie: UNSUPPORTED, ios_saf: 983040, opera: 2097152, safari: 983040, samsung: 327680 }), // TradChineseInformalListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 524288, ios_saf: 65536, opera: 655616, safari: 65536, samsung: 65536 }), // UpperAlphaListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 5177344, firefox: 2162688, ie: UNSUPPORTED, ios_saf: 327680, opera: 917504, safari: 327936, samsung: 65536 }), // UpperArmenianListStyleType + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // UpperHexadecimalListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 524288, ios_saf: 65536, opera: 655616, safari: 65536, samsung: 65536 }), // UpperLatinListStyleType + Some(MinVersions { android: 196608, chrome: 1179648, edge: 5963776, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 262656, opera: 917504, safari: 327680, samsung: 65536 }), // UpperNorwegianListStyleType + Some(MinVersions { android: 263168, chrome: 1179648, edge: 786432, firefox: 262144, ie: 524288, ios_saf: 65536, opera: 655616, safari: 65536, samsung: 65536 }), // UpperRomanListStyleType + Some(MinVersions { android: 7077888, chrome: 7077888, edge: 7077888, firefox: 6619136, ie: UNSUPPORTED, ios_saf: 984064, opera: 4784128, safari: 984064, samsung: 1376256 }), // VbUnit + Some(MinVersions { android: 263168, chrome: 1638400, edge: 786432, firefox: 1245184, ie: 589824, ios_saf: 393216, opera: 917504, safari: 393216, samsung: 66816 }), // VhUnit + Some(MinVersions { android: 7077888, chrome: 7077888, edge: 7077888, firefox: 6619136, ie: UNSUPPORTED, ios_saf: 984064, opera: 4784128, safari: 984064, samsung: 1376256 }), // ViUnit + Some(MinVersions { android: 7077888, chrome: 7077888, edge: 7077888, firefox: 6619136, ie: UNSUPPORTED, ios_saf: 984064, opera: 4784128, safari: 984064, samsung: 1376256 }), // ViewportPercentageUnitsDynamic + Some(MinVersions { android: 7077888, chrome: 7077888, edge: 7077888, firefox: 6619136, ie: UNSUPPORTED, ios_saf: 984064, opera: 4784128, safari: 984064, samsung: 1376256 }), // ViewportPercentageUnitsLarge + Some(MinVersions { android: 7077888, chrome: 7077888, edge: 7077888, firefox: 6619136, ie: UNSUPPORTED, ios_saf: 984064, opera: 4784128, safari: 984064, samsung: 1376256 }), // ViewportPercentageUnitsSmall + Some(MinVersions { android: 66816, chrome: 1703936, edge: 1048576, firefox: 1245184, ie: UNSUPPORTED, ios_saf: 458752, opera: 917504, safari: 458752, samsung: 66816 }), // VmaxUnit + Some(MinVersions { android: 263168, chrome: 1703936, edge: 786432, firefox: 1245184, ie: 655360, ios_saf: 458752, opera: 917504, safari: 458752, samsung: 66816 }), // VminUnit + Some(MinVersions { android: 263168, chrome: 1638400, edge: 786432, firefox: 1245184, ie: 589824, ios_saf: 393216, opera: 917504, safari: 393216, samsung: 66816 }), // VwUnit + Some(MinVersions { android: 263168, chrome: 1638400, edge: 5177344, firefox: UNSUPPORTED, ie: UNSUPPORTED, ios_saf: 458752, opera: 917504, safari: 458752, samsung: 327680 }), // WebkitFillAvailableSize + Some(MinVersions { android: 4456448, chrome: 4456448, edge: 5177344, firefox: 4063232, ie: UNSUPPORTED, ios_saf: UNSUPPORTED, opera: 3145728, safari: UNSUPPORTED, samsung: 655360 }), // XResolutionUnit +]; + impl Feature { /// Returns whether all of the given browser targets support this feature /// natively, without fallback. - #[allow(clippy::match_same_arms)] pub fn is_compatible(self, browsers: &Browsers) -> bool { - match self { - Feature::Selectors2 => { - if let Some(version) = browsers.ie { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 131072 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 196864 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 197120 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 131328 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 262144 { - return false; - } - } - } - Feature::Selectors3 => { - if let Some(version) = browsers.ie { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 197888 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 197120 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 591104 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 197120 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 131328 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 262144 { - return false; - } - } - } - Feature::Gencontent | Feature::FirstLine => { - if let Some(version) = browsers.ie { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 131072 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 196864 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 197120 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 131328 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 262144 { - return false; - } - } - } - Feature::FirstLetter => { - if let Some(version) = browsers.ie { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 197888 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 327936 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 722432 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 196608 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 262144 { - return false; - } - } - } - Feature::InOutOfRange => { - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 3276800 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 3473408 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 655616 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 2621440 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 656128 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 327680 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::FormValidation => { - if let Some(version) = browsers.ie { - if version < 655360 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 655360 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 655616 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 655360 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 656128 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263171 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 262144 { - return false; - } - } - } - Feature::AnyLink => { - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 3276800 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 4259840 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 3407872 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 590336 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::DefaultPseudo => { - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 3342336 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 655616 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 2490368 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 656128 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 327680 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::DirSelector => { - if let Some(version) = browsers.edge { - if version < 7864320 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 3211264 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 7864320 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 1049600 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 6946816 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 1049600 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1638400 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::FocusWithin => { - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 3407872 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 3932160 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 655616 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 3080192 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 656128 { + let Some(min) = &MIN_VERSIONS[self as usize] else { + return false; + }; + macro_rules! check { + ($field:ident) => { + if let Some(version) = browsers.$field { + if min.$field == UNSUPPORTED || version < min.$field { return false; } } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 524800 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::FocusVisible => { - if let Some(version) = browsers.edge { - if version < 5636096 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 5570560 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 5636096 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 984064 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4718592 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 984064 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 917504 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::IndeterminatePseudo => { - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 3342336 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 2555904 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 655616 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 1703936 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 656128 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 262144 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::IsSelector => { - if let Some(version) = browsers.edge { - if version < 5767168 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 5111808 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 5767168 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4915200 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 983040 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::OptionalPseudo => { - if let Some(version) = browsers.ie { - if version < 655360 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 131840 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 262144 { - return false; - } - } - } - Feature::PlaceholderShown => { - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 3342336 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 3080192 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 2228224 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 327680 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::Dialog => { - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 6422528 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 2424832 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 984064 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 1572864 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 984064 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 262144 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::Fullscreen => { - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 4194304 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 4653056 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 1049600 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 786688 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 655616 { - return false; - } - } - if browsers.android.is_some() || browsers.ie.is_some() || browsers.ios_saf.is_some() - { - return false; - } - } - Feature::MarkerPseudo => { - if let Some(version) = browsers.edge { - if version < 5636096 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 4456448 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 5636096 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4718592 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 917504 { - return false; - } - } - if browsers.ie.is_some() || browsers.ios_saf.is_some() || browsers.safari.is_some() - { - return false; - } - } - Feature::Placeholder => { - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 3342336 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 3735552 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 655616 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 2883584 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 656128 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 459264 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::Selection => { - if let Some(version) = browsers.ie { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 4063232 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 196864 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 591104 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 262144 { - return false; - } - } - if browsers.ios_saf.is_some() { - return false; - } - } - Feature::CaseInsensitive => { - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 3080192 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 3211264 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 2359296 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 327680 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::ReadOnlyWrite => { - if let Some(version) = browsers.edge { - if version < 851968 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 5111808 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 2359296 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 1507328 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 262144 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::Autofill => { - if let Some(version) = browsers.chrome { - if version < 7208960 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 7208960 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 5636096 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 6291456 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1376256 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::Namespaces => { - if let Some(version) = browsers.ie { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 131072 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 262656 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 131328 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 262144 { - return false; - } - } - } - Feature::Shadowdomv1 => { - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 4128768 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 3473408 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 655360 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 2621440 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 393728 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::HexAlphaColors => { - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 3211264 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 4063232 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 655360 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 3407872 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 655360 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 524800 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::Nesting => { - if let Some(version) = browsers.edge { - if version < 7864320 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 7667712 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 7864320 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 1114624 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 6946816 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 1114624 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if browsers.ie.is_some() || browsers.samsung.is_some() { - return false; - } - } - Feature::NotSelectorList => { - if let Some(version) = browsers.edge { - if version < 5767168 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 5505024 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 5767168 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4915200 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 983040 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::HasSelector => { - if let Some(version) = browsers.edge { - if version < 6881280 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 7929856 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 6881280 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 984064 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 5963776 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 984064 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1310720 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::FontFamilySystemUi => { - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 6029312 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 3670016 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 2818048 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 393728 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::ExtendedSystemFonts => { - if let Some(version) = browsers.safari { - if version < 852224 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 852992 { - return false; - } - } - if browsers.android.is_some() - || browsers.chrome.is_some() - || browsers.edge.is_some() - || browsers.firefox.is_some() - || browsers.ie.is_some() - || browsers.opera.is_some() - || browsers.samsung.is_some() - { - return false; - } - } - Feature::CalcFunction => { - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 1048576 { - return false; - } - } - if let Some(version) = browsers.chrome { - if version < 1703936 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 393472 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8585216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 262144 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::CustomMediaQueries - | Feature::FitContentFunctionSize - | Feature::StretchSize => { - return false; - } - Feature::DoublePositionGradients => { - if let Some(version) = browsers.chrome { - if version < 4653056 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 4194304 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 3276800 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 786688 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 786944 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 655360 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 4653056 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::ClampFunction => { - if let Some(version) = browsers.chrome { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 3735552 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 852224 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 852992 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 5177344 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::PlaceSelf | Feature::PlaceItems => { - if let Some(version) = browsers.chrome { - if version < 3866624 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 2949120 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 2818048 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 3866624 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::PlaceContent => { - if let Some(version) = browsers.chrome { - if version < 3866624 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 2949120 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 2818048 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 3866624 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::OverflowShorthand => { - if let Some(version) = browsers.chrome { - if version < 4456448 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 3997696 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 3145728 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 852224 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 852992 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 655360 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 4456448 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::MediaRangeSyntax => { - if let Some(version) = browsers.chrome { - if version < 6815744 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 6815744 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 4128768 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4653056 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 1049600 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 1049600 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1310720 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 6815744 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::MediaIntervalSyntax => { - if let Some(version) = browsers.chrome { - if version < 6815744 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 6815744 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 6684672 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4653056 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 1049600 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 1049600 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1310720 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 6815744 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::LogicalBorders => { - if let Some(version) = browsers.chrome { - if version < 4521984 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 2686976 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 3145728 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 786688 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 786944 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 655360 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 4521984 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::LogicalBorderShorthand - | Feature::LogicalMarginShorthand - | Feature::LogicalPaddingShorthand => { - if let Some(version) = browsers.chrome { - if version < 5701632 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5701632 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 4325376 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4063232 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 917760 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 918784 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 5701632 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::LogicalBorderRadius => { - if let Some(version) = browsers.chrome { - if version < 5832704 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5832704 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 4325376 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4128768 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 5832704 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::LogicalMargin | Feature::LogicalPadding => { - if let Some(version) = browsers.chrome { - if version < 4521984 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 2686976 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 3145728 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 786688 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 786944 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 655360 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 5701632 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::LogicalInset => { - if let Some(version) = browsers.chrome { - if version < 5701632 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5701632 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 4128768 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4063232 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 917760 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 918784 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 5701632 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::LogicalSize => { - if let Some(version) = browsers.chrome { - if version < 3735552 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 2686976 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 2818048 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 786688 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 786944 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 3735552 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::LogicalTextAlign => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 196864 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 131072 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 2424832 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::LabColors => { - if let Some(version) = browsers.chrome { - if version < 7274496 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 7274496 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 7405568 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4915200 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1441792 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 7274496 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::OklabColors => { - if let Some(version) = browsers.chrome { - if version < 7274496 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 7274496 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 7405568 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4915200 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 984064 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 984064 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1441792 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 7274496 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::ColorFunction => { - if let Some(version) = browsers.chrome { - if version < 7274496 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 7274496 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 7405568 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4915200 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 655616 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 656128 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1441792 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 7274496 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::SpaceSeparatedColorNotation => { - if let Some(version) = browsers.chrome { - if version < 4259840 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 3407872 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 3080192 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 786688 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 786944 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 4259840 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::TextDecorationThicknessPercent => { - if let Some(version) = browsers.chrome { - if version < 5701632 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5701632 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4063232 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 1115136 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 1115136 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 5701632 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::TextDecorationThicknessShorthand => { - if let Some(version) = browsers.chrome { - if version < 5701632 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5701632 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4063232 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 5701632 { - return false; - } - } - if browsers.ie.is_some() || browsers.ios_saf.is_some() || browsers.safari.is_some() - { - return false; - } - } - Feature::Cue => { - if let Some(version) = browsers.chrome { - if version < 1703936 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 3604480 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 66816 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::CueFunction => { - if let Some(version) = browsers.chrome { - if version < 1703936 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 66816 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - if browsers.firefox.is_some() || browsers.ie.is_some() { - return false; - } - } - Feature::AnyPseudo => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 2424832 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::PartPseudo => { - if let Some(version) = browsers.chrome { - if version < 4784128 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 3407872 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 852224 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 852992 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 4784128 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::ImageSet => { - if let Some(version) = browsers.chrome { - if version < 1638400 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 5767168 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 393216 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 393216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 66816 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::XResolutionUnit => { - if let Some(version) = browsers.chrome { - if version < 4456448 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 4063232 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 3145728 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 655360 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 4456448 { - return false; - } - } - if browsers.ie.is_some() || browsers.ios_saf.is_some() || browsers.safari.is_some() - { - return false; - } - } - Feature::NthChildOf => { - if let Some(version) = browsers.chrome { - if version < 7274496 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 7274496 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 7405568 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4915200 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1441792 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 7274496 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::MinFunction | Feature::MaxFunction => { - if let Some(version) = browsers.chrome { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 3735552 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 721152 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 721664 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 5177344 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::RoundFunction | Feature::RemFunction | Feature::ModFunction => { - if let Some(version) = browsers.chrome { - if version < 8192000 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 8192000 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 7733248 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 5439488 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 984064 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 984064 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8192000 { - return false; - } - } - if browsers.ie.is_some() || browsers.samsung.is_some() { - return false; - } - } - Feature::AbsFunction | Feature::SignFunction => { - if let Some(version) = browsers.firefox { - if version < 7733248 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 984064 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 984064 { - return false; - } - } - if browsers.android.is_some() - || browsers.chrome.is_some() - || browsers.edge.is_some() - || browsers.ie.is_some() - || browsers.opera.is_some() - || browsers.samsung.is_some() - { - return false; - } - } - Feature::HypotFunction => { - if let Some(version) = browsers.chrome { - if version < 7864320 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 7864320 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 7733248 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 5242880 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 984064 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 984064 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1638400 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 7864320 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::GradientInterpolationHints => { - if let Some(version) = browsers.chrome { - if version < 2621440 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 2359296 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 1769472 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 2621440 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::BorderImageRepeatRound => { - if let Some(version) = browsers.chrome { - if version < 1966080 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 590080 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 590592 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 131072 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - } - Feature::BorderImageRepeatSpace => { - if let Some(version) = browsers.chrome { - if version < 3670016 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 3276800 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 2818048 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 590080 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 590592 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 393216 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 3670016 { - return false; - } - } - } - Feature::FontSizeRem => { - if let Some(version) = browsers.chrome { - if version < 2752512 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 2031616 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 1835008 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 2752512 { - return false; - } - } - } - Feature::FontSizeXXXLarge => { - if let Some(version) = browsers.chrome { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 3735552 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 1049600 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 1049600 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 5177344 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::FontStyleObliqueAngle => { - if let Some(version) = browsers.chrome { - if version < 4063232 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 3997696 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 3014656 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 721152 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 721664 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 524288 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 4063232 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::FontWeightNumber => { - if let Some(version) = browsers.chrome { - if version < 4063232 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 1114112 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 3997696 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 3014656 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 524288 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 4063232 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::FontStretchPercentage => { - if let Some(version) = browsers.chrome { - if version < 4063232 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 3997696 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 3014656 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 721152 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 721664 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 524288 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 4063232 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::LightDark => { - if let Some(version) = browsers.chrome { - if version < 8060928 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 8060928 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 7864320 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 5373952 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 1115392 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 1115392 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8060928 { - return false; - } - } - if browsers.ie.is_some() || browsers.samsung.is_some() { - return false; - } - } - Feature::AccentSystemColor => { - if let Some(version) = browsers.firefox { - if version < 6750208 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 1049856 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 1049856 { - return false; - } - } - if browsers.android.is_some() - || browsers.chrome.is_some() - || browsers.edge.is_some() - || browsers.ie.is_some() - || browsers.opera.is_some() - || browsers.samsung.is_some() - { - return false; - } - } - Feature::AnimationTimelineShorthand => { - if let Some(version) = browsers.chrome { - if version < 7536640 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 7536640 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 5046272 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1507328 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 7536640 { - return false; - } - } - if browsers.firefox.is_some() - || browsers.ie.is_some() - || browsers.ios_saf.is_some() - || browsers.safari.is_some() - { - return false; - } - } - Feature::QUnit => { - if let Some(version) = browsers.chrome { - if version < 4128768 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 3211264 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 3014656 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 852224 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 852992 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 524288 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 4128768 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::CapUnit => { - if let Some(version) = browsers.chrome { - if version < 7733248 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 7733248 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 6356992 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 1114624 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 1114624 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1638400 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 7733248 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::ChUnit => { - if let Some(version) = browsers.chrome { - if version < 1769472 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 66816 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - } - Feature::ContainerQueryLengthUnits => { - if let Some(version) = browsers.chrome { - if version < 6881280 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 6881280 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 7208960 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4718592 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 1048576 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 1048576 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1310720 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 6881280 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::EmUnit => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 196608 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 655616 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 65536 { - return false; - } - } - } - Feature::ExUnit - | Feature::CircleListStyleType - | Feature::DecimalListStyleType - | Feature::DiscListStyleType - | Feature::SquareListStyleType => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 655616 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - } - Feature::IcUnit => { - if let Some(version) = browsers.chrome { - if version < 6946816 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 6946816 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 6356992 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4718592 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 984064 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 984064 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1310720 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 6946816 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::LhUnit => { - if let Some(version) = browsers.chrome { - if version < 7143424 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 7143424 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 7864320 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4849664 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 1049600 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 1049600 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1376256 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 7143424 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::RcapUnit => { - if let Some(version) = browsers.chrome { - if version < 7733248 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 7733248 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 1114624 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 1114624 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1638400 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 7733248 { - return false; - } - } - if browsers.firefox.is_some() || browsers.ie.is_some() { - return false; - } - } - Feature::RchUnit | Feature::RexUnit | Feature::RicUnit => { - if let Some(version) = browsers.chrome { - if version < 7274496 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 7274496 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4915200 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 1114624 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 1114624 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1441792 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 7274496 { - return false; - } - } - if browsers.firefox.is_some() || browsers.ie.is_some() { - return false; - } - } - Feature::RemUnit => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 131072 { - return false; - } - } - } - Feature::RlhUnit => { - if let Some(version) = browsers.chrome { - if version < 7274496 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 7274496 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 7864320 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4915200 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 1049600 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 1049600 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1441792 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 7274496 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::VbUnit - | Feature::ViUnit - | Feature::ViewportPercentageUnitsDynamic - | Feature::ViewportPercentageUnitsLarge - | Feature::ViewportPercentageUnitsSmall => { - if let Some(version) = browsers.chrome { - if version < 7077888 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 7077888 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 6619136 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4784128 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 984064 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 984064 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1376256 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 7077888 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::VhUnit | Feature::VwUnit => { - if let Some(version) = browsers.chrome { - if version < 1638400 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 1245184 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 589824 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 393216 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 393216 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 66816 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - } - Feature::VmaxUnit => { - if let Some(version) = browsers.chrome { - if version < 1703936 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 1048576 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 1245184 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 66816 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 66816 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::VminUnit => { - if let Some(version) = browsers.chrome { - if version < 1703936 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 1245184 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 655360 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 66816 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - } - Feature::ConicGradient | Feature::RepeatingConicGradient => { - if let Some(version) = browsers.chrome { - if version < 4521984 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 5439488 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 3145728 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 786688 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 786944 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 655360 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 4521984 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::LinearGradient | Feature::RepeatingLinearGradient => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 655360 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 327936 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 2424832 { - return false; - } - } - } - Feature::RadialGradient => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 655360 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 327936 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 2424832 { - return false; - } - } - } - Feature::RepeatingRadialGradient => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 655360 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 655360 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 327936 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - } - Feature::AfarListStyleType - | Feature::AmharicListStyleType - | Feature::AmharicAbegedeListStyleType - | Feature::EthiopicListStyleType - | Feature::EthiopicAbegedeListStyleType - | Feature::EthiopicAbegedeAmEtListStyleType - | Feature::EthiopicAbegedeGezListStyleType - | Feature::EthiopicAbegedeTiErListStyleType - | Feature::EthiopicAbegedeTiEtListStyleType - | Feature::EthiopicHalehameAaErListStyleType - | Feature::EthiopicHalehameAaEtListStyleType - | Feature::EthiopicHalehameAmEtListStyleType - | Feature::EthiopicHalehameGezListStyleType - | Feature::EthiopicHalehameOmEtListStyleType - | Feature::EthiopicHalehameSidEtListStyleType - | Feature::EthiopicHalehameSoEtListStyleType - | Feature::EthiopicHalehameTigListStyleType - | Feature::LowerHexadecimalListStyleType - | Feature::LowerNorwegianListStyleType - | Feature::UpperHexadecimalListStyleType - | Feature::UpperNorwegianListStyleType => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5963776 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 262656 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 196608 { - return false; - } - } - if browsers.firefox.is_some() || browsers.ie.is_some() { - return false; - } - } - Feature::ArabicIndicListStyleType - | Feature::BengaliListStyleType - | Feature::CjkEarthlyBranchListStyleType - | Feature::CjkHeavenlyStemListStyleType - | Feature::DevanagariListStyleType - | Feature::GujaratiListStyleType - | Feature::GurmukhiListStyleType - | Feature::KannadaListStyleType - | Feature::KhmerListStyleType - | Feature::LaoListStyleType - | Feature::MalayalamListStyleType - | Feature::MyanmarListStyleType - | Feature::OriyaListStyleType - | Feature::PersianListStyleType - | Feature::TeluguListStyleType - | Feature::ThaiListStyleType => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 262656 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::ArmenianListStyleType - | Feature::DecimalLeadingZeroListStyleType - | Feature::GeorgianListStyleType - | Feature::LowerAlphaListStyleType - | Feature::LowerGreekListStyleType - | Feature::LowerRomanListStyleType - | Feature::UpperAlphaListStyleType - | Feature::UpperLatinListStyleType - | Feature::UpperRomanListStyleType => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 524288 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 655616 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - } - Feature::AsterisksListStyleType | Feature::FootnotesListStyleType => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5963776 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 327936 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - if browsers.firefox.is_some() || browsers.ie.is_some() { - return false; - } - } - Feature::BinaryListStyleType - | Feature::OctalListStyleType - | Feature::OromoListStyleType - | Feature::SidamaListStyleType - | Feature::SomaliListStyleType - | Feature::TigreListStyleType - | Feature::TigrinyaErListStyleType - | Feature::TigrinyaErAbegedeListStyleType - | Feature::TigrinyaEtListStyleType - | Feature::TigrinyaEtAbegedeListStyleType => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5963776 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 262656 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - if browsers.firefox.is_some() || browsers.ie.is_some() { - return false; - } - } - Feature::CambodianListStyleType - | Feature::MongolianListStyleType - | Feature::TibetanListStyleType => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 2162688 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 262656 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::CjkDecimalListStyleType => { - if let Some(version) = browsers.chrome { - if version < 5963776 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5963776 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 1835008 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4194304 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1048576 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 5963776 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::DisclosureClosedListStyleType | Feature::DisclosureOpenListStyleType => { - if let Some(version) = browsers.chrome { - if version < 5832704 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5832704 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 2162688 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4128768 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 5832704 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::EthiopicNumericListStyleType - | Feature::JapaneseFormalListStyleType - | Feature::JapaneseInformalListStyleType - | Feature::TamilListStyleType => { - if let Some(version) = browsers.chrome { - if version < 5963776 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5963776 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 4194304 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 1048576 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 5963776 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::HebrewListStyleType - | Feature::HiraganaListStyleType - | Feature::HiraganaIrohaListStyleType - | Feature::KatakanaListStyleType - | Feature::KatakanaIrohaListStyleType - | Feature::AutoSize => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - } - Feature::KoreanHangulFormalListStyleType - | Feature::KoreanHanjaFormalListStyleType - | Feature::KoreanHanjaInformalListStyleType => { - if let Some(version) = browsers.chrome { - if version < 2949120 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 1835008 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 2097152 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 2949120 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::LowerArmenianListStyleType | Feature::UpperArmenianListStyleType => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 2162688 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 327936 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::LowerLatinListStyleType => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 524288 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 655616 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - } - Feature::NoneListStyleType => { - if let Some(version) = browsers.chrome { - if version < 1179648 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 65536 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - } - Feature::SimpChineseFormalListStyleType - | Feature::SimpChineseInformalListStyleType - | Feature::TradChineseFormalListStyleType - | Feature::TradChineseInformalListStyleType => { - if let Some(version) = browsers.chrome { - if version < 2949120 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 2097152 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 983040 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 2949120 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::StringListStyleType => { - if let Some(version) = browsers.chrome { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 2555904 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 3735552 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 917760 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 918784 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 5177344 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::SymbolsListStyleType => { - if let Some(version) = browsers.firefox { - if version < 2293760 { - return false; - } - } - if browsers.android.is_some() - || browsers.chrome.is_some() - || browsers.edge.is_some() - || browsers.ie.is_some() - || browsers.ios_saf.is_some() - || browsers.opera.is_some() - || browsers.safari.is_some() - || browsers.samsung.is_some() - { - return false; - } - } - Feature::AnchorSizeSize => { - if let Some(version) = browsers.chrome { - if version < 8192000 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 8192000 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 5439488 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 8192000 { - return false; - } - } - if browsers.firefox.is_some() - || browsers.ie.is_some() - || browsers.ios_saf.is_some() - || browsers.safari.is_some() - || browsers.samsung.is_some() - { - return false; - } - } - Feature::FitContentSize => { - if let Some(version) = browsers.chrome { - if version < 1638400 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 66816 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::IsAnimatableSize => { - if let Some(version) = browsers.chrome { - if version < 1703936 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 786432 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 1048576 { - return false; - } - } - if let Some(version) = browsers.ie { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 66816 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - } - Feature::MaxContentSize => { - if let Some(version) = browsers.chrome { - if version < 1638400 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 2818048 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 66816 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::MinContentSize => { - if let Some(version) = browsers.chrome { - if version < 3014656 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 2162688 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 720896 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 3014656 { - return false; - } - } - if browsers.ie.is_some() { - return false; - } - } - Feature::WebkitFillAvailableSize => { - if let Some(version) = browsers.chrome { - if version < 1638400 { - return false; - } - } - if let Some(version) = browsers.edge { - if version < 5177344 { - return false; - } - } - if let Some(version) = browsers.opera { - if version < 917504 { - return false; - } - } - if let Some(version) = browsers.safari { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 458752 { - return false; - } - } - if let Some(version) = browsers.samsung { - if version < 327680 { - return false; - } - } - if let Some(version) = browsers.android { - if version < 263168 { - return false; - } - } - if browsers.firefox.is_some() || browsers.ie.is_some() { - return false; - } - } - Feature::MozAvailableSize => { - if let Some(version) = browsers.firefox { - if version < 262144 { - return false; - } - } - if browsers.android.is_some() - || browsers.chrome.is_some() - || browsers.edge.is_some() - || browsers.ie.is_some() - || browsers.ios_saf.is_some() - || browsers.opera.is_some() - || browsers.safari.is_some() - || browsers.samsung.is_some() - { - return false; - } - } - Feature::P3Colors | Feature::LangSelectorList => { - if let Some(version) = browsers.safari { - if version < 655616 { - return false; - } - } - if let Some(version) = browsers.ios_saf { - if version < 656128 { - return false; - } - } - if browsers.android.is_some() - || browsers.chrome.is_some() - || browsers.edge.is_some() - || browsers.firefox.is_some() - || browsers.ie.is_some() - || browsers.opera.is_some() - || browsers.samsung.is_some() - { - return false; - } - } + }; } + check!(android); + check!(chrome); + check!(edge); + check!(firefox); + check!(ie); + check!(ios_saf); + check!(opera); + check!(safari); + check!(samsung); true } /// Returns whether *any* of the given browser targets supports this /// feature natively. pub fn is_partially_compatible(self, targets: &Browsers) -> bool { - // Generic implementation in terms of `is_compatible` — - // probe each browser field one at a time. - macro_rules! probe { + let Some(min) = &MIN_VERSIONS[self as usize] else { + return false; + }; + macro_rules! check { ($field:ident) => { - if targets.$field.is_some() { - let mut browsers = Browsers::default(); - browsers.$field = targets.$field; - if self.is_compatible(&browsers) { + if let Some(version) = targets.$field { + if min.$field != UNSUPPORTED && version >= min.$field { return true; } } }; } - probe!(android); - probe!(chrome); - probe!(edge); - probe!(firefox); - probe!(ie); - probe!(ios_saf); - probe!(opera); - probe!(safari); - probe!(samsung); + check!(android); + check!(chrome); + check!(edge); + check!(firefox); + check!(ie); + check!(ios_saf); + check!(opera); + check!(safari); + check!(samsung); false } } diff --git a/src/css/css_parser.rs b/src/css/css_parser.rs index 3bdf0af56f07..62c42aaefe0c 100644 --- a/src/css/css_parser.rs +++ b/src/css/css_parser.rs @@ -694,16 +694,24 @@ pub trait QualifiedRuleParser { /// Parse the prelude of a qualified rule. For style rules, this is a /// Selector list. The given `input` is a "delimited" parser that ends /// where the prelude should end (before the next `{`). - fn parse_prelude(this: &mut Self, input: &mut Parser) -> CssResult; + /// + /// The default implementation rejects all qualified rules. + fn parse_prelude(_this: &mut Self, input: &mut Parser) -> CssResult { + Err(input.new_error(BasicParseErrorKind::qualified_rule_invalid)) + } /// Parse the content of a `{ /* ... */ }` block for the body of the /// qualified rule. + /// + /// The default implementation rejects all qualified rules. fn parse_block( - this: &mut Self, - prelude: Self::Prelude, - start: &ParserState, + _this: &mut Self, + _prelude: Self::Prelude, + _start: &ParserState, input: &mut Parser, - ) -> CssResult; + ) -> CssResult { + Err(input.new_error(BasicParseErrorKind::qualified_rule_invalid)) + } } #[derive(Default, Clone, Copy, crate::DeepClone)] @@ -766,22 +774,34 @@ pub trait CustomAtRuleParser { } /// At rules are rules that have the `@` symbol. +/// +/// The default method implementations reject all at-rules. pub trait AtRuleParser { type Prelude; type AtRule; - fn parse_prelude(this: &mut Self, name: &[u8], input: &mut Parser) -> CssResult; + fn parse_prelude( + _this: &mut Self, + name: &[u8], + input: &mut Parser, + ) -> CssResult { + Err(input.new_error(BasicParseErrorKind::at_rule_invalid(name))) + } fn rule_without_block( - this: &mut Self, - prelude: Self::Prelude, - start: &ParserState, - ) -> Maybe; + _this: &mut Self, + _prelude: Self::Prelude, + _start: &ParserState, + ) -> Maybe { + Err(()) + } fn parse_block( - this: &mut Self, - prelude: Self::Prelude, - start: &ParserState, + _this: &mut Self, + _prelude: Self::Prelude, + _start: &ParserState, input: &mut Parser, - ) -> CssResult; + ) -> CssResult { + Err(input.new_error(BasicParseErrorKind::at_rule_body_invalid)) + } } #[derive(Default)] diff --git a/src/css/declaration.rs b/src/css/declaration.rs index 41b7e5ccce50..de7a0b017811 100644 --- a/src/css/declaration.rs +++ b/src/css/declaration.rs @@ -324,49 +324,11 @@ pub(crate) struct PropertyDeclarationParser<'a, 'bump> { impl<'a, 'bump> css::AtRuleParser for PropertyDeclarationParser<'a, 'bump> { type Prelude = (); type AtRule = (); - - fn parse_prelude( - _this: &mut Self, - name: &[u8], - input: &mut css::Parser, - ) -> Result { - Err(input.new_error(css::BasicParseErrorKind::at_rule_invalid(name))) - } - - fn parse_block( - _this: &mut Self, - _: Self::Prelude, - _: &css::ParserState, - input: &mut css::Parser, - ) -> Result { - Err(input.new_error(css::BasicParseErrorKind::at_rule_body_invalid)) - } - - fn rule_without_block( - _this: &mut Self, - _: Self::Prelude, - _: &css::ParserState, - ) -> css::Maybe { - Err(()) - } } impl<'a, 'bump> css::QualifiedRuleParser for PropertyDeclarationParser<'a, 'bump> { type Prelude = (); type QualifiedRule = (); - - fn parse_prelude(_this: &mut Self, input: &mut css::Parser) -> Result { - Err(input.new_error(css::BasicParseErrorKind::qualified_rule_invalid)) - } - - fn parse_block( - _this: &mut Self, - _prelude: Self::Prelude, - _start: &css::ParserState, - input: &mut css::Parser, - ) -> Result { - Err(input.new_error(css::BasicParseErrorKind::qualified_rule_invalid)) - } } impl<'a, 'bump> css::DeclarationParser for PropertyDeclarationParser<'a, 'bump> { diff --git a/src/css/properties/custom.rs b/src/css/properties/custom.rs index 4251ecf3b610..bcf93586b8b9 100644 --- a/src/css/properties/custom.rs +++ b/src/css/properties/custom.rs @@ -6,7 +6,7 @@ // `EnvironmentVariableName::{parse, to_css}`, `Function::to_css`, // `CustomProperty::parse`, `UnparsedProperty::parse` are now real. // -// A few leaf calls (Url::parse/to_css, CustomIdent::to_css) are still +// A few leaf calls (Url::parse, CustomIdent::to_css) are still // ``-gated in *other* files; those bodies are inlined verbatim // under `mod ext` below so the hub compiles without touching // `values/{url,ident}.rs`. `DashedIdentReference::{parse_with_options,to_css}` @@ -45,7 +45,7 @@ use crate::generics::{CssEql, CssHash, DeepClone}; use bun_alloc::Arena; // ─── External-gate shims ─────────────────────────────────────────────────── -// `TokenList::{parse,to_css}` bottom out on a handful of leaf fns that still +// `TokenList::parse` bottoms out on a handful of leaf fns that still // carry `` in *other* files (`values/{url,ident}.rs`, // `css_modules.rs`). Those gates are stale — every dependency they cite now // exists — but this round's edit scope is `custom.rs` + `css_parser.rs` only. @@ -73,76 +73,6 @@ mod ext { }) } - /// Inline of `Url::to_css` (gated in `values/url.rs` on `WriteAll for - /// Vec`, which this round adds in css_parser.rs). - pub(super) fn url_to_css(this: &Url, dest: &mut Printer) -> PrintResult<()> { - let dep: Option = if dest.dependencies.is_some() { - // `get_import_records` borrows &mut *dest, so capture - // arena/filename first. - let arena = dest.arena; - // SAFETY: filename borrows the printer arena/options which outlive `dest`. - let filename: &[u8] = unsafe { &*std::ptr::from_ref::<[u8]>(dest.filename()) }; - let records = dest.get_import_records()?; - Some(dependencies::UrlDependency::new( - arena, this, filename, records, - )) - } else { - None - }; - - // If adding dependencies, always write url() with quotes so that the placeholder can - // be replaced without escaping more easily. Quotes may be removed later during minification. - if let Some(d) = dep { - dest.write_str("url(")?; - // SAFETY: placeholder borrows the printer arena. - let placeholder = unsafe { crate::arena_str(d.placeholder) }; - dest.serialize_string(placeholder)?; - dest.write_char(b')')?; - - if let Some(dependencies) = &mut dest.dependencies { - dependencies.push(crate::Dependency::Url(d)); - } - - return Ok(()); - } - - let import_record = dest.import_record(this.import_record_idx)?; - let is_internal = import_record.tag.is_internal(); - // `get_import_record_url` reborrows &mut *dest, so capture - // `is_internal` first. - let url: &'static [u8] = { - let u = dest.get_import_record_url(this.import_record_idx)?; - // SAFETY: import-record paths are arena/source-owned and outlive `dest`. - unsafe { &*std::ptr::from_ref::<[u8]>(u) } - }; - - if dest.minify && !is_internal { - let mut buf: Vec = Vec::new(); - // PERF(alloc) we could use stack fallback here? - let _ = Token::UnquotedUrl(url).to_css_generic(&mut buf); - - // If the unquoted url is longer than it would be quoted (e.g. `url("...")`) - // then serialize as a string and choose the shorter version. - if buf.len() > url.len() + 7 { - let mut buf2: Vec = Vec::new(); - // PERF(alloc) we could use stack fallback here? - let _ = css_parser::serializer::serialize_string(url, &mut buf2); - if buf2.len() + 5 < buf.len() { - dest.write_str("url(")?; - dest.write_str(&buf2)?; - return dest.write_char(b')'); - } - } - - dest.write_str(&buf)?; - } else { - dest.write_str("url(")?; - dest.serialize_string(url)?; - dest.write_char(b')')?; - } - Ok(()) - } - /// Forwarder to `DashedIdentReference::parse_with_options` (now un-gated /// in `values/ident.rs`). Honors `options.css_modules.dashed_idents` and /// parses the `from ` suffix when enabled. @@ -348,7 +278,7 @@ impl TokenList { Some(url.loc), ); } - ext::url_to_css(url, dest)?; + url.to_css(dest)?; has_whitespace = false; } TokenOrValue::Var(var) => { diff --git a/src/css/rules/font_face.rs b/src/css/rules/font_face.rs index b26d7f28e05e..4d15ebba2ff8 100644 --- a/src/css/rules/font_face.rs +++ b/src/css/rules/font_face.rs @@ -707,58 +707,16 @@ const _: () = { use css::css_parser::{ AtRuleParser, DeclarationParser, QualifiedRuleParser, RuleBodyItemParser, }; - use css::{BasicParseErrorKind, Maybe, Parser, ParserOptions, ParserState, Result}; + use css::{Parser, ParserOptions, Result}; impl AtRuleParser for FontFaceDeclarationParser { type Prelude = (); type AtRule = FontFaceProperty; - - fn parse_prelude( - _this: &mut Self, - name: &[u8], - input: &mut Parser, - ) -> Result { - Err( - input.new_error(BasicParseErrorKind::at_rule_invalid(std::ptr::from_ref::< - [u8], - >(name))), - ) - } - - fn parse_block( - _this: &mut Self, - _: Self::Prelude, - _: &ParserState, - input: &mut Parser, - ) -> Result { - Err(input.new_error(BasicParseErrorKind::at_rule_body_invalid)) - } - - fn rule_without_block( - _this: &mut Self, - _: Self::Prelude, - _: &ParserState, - ) -> Maybe { - Err(()) - } } impl QualifiedRuleParser for FontFaceDeclarationParser { type Prelude = (); type QualifiedRule = FontFaceProperty; - - fn parse_prelude(_this: &mut Self, input: &mut Parser) -> Result { - Err(input.new_error(BasicParseErrorKind::qualified_rule_invalid)) - } - - fn parse_block( - _this: &mut Self, - _: Self::Prelude, - _: &ParserState, - input: &mut Parser, - ) -> Result { - Err(input.new_error(BasicParseErrorKind::qualified_rule_invalid)) - } } impl DeclarationParser for FontFaceDeclarationParser { diff --git a/src/css/rules/font_palette_values.rs b/src/css/rules/font_palette_values.rs index 45258ff8c984..fdfc1f19313b 100644 --- a/src/css/rules/font_palette_values.rs +++ b/src/css/rules/font_palette_values.rs @@ -236,9 +236,7 @@ const _: () = { use css::css_parser::{ AtRuleParser, DeclarationParser, QualifiedRuleParser, RuleBodyItemParser, }; - use css::{ - BasicParseErrorKind, Maybe, Parser, ParserError, ParserOptions, ParserState, Result, - }; + use css::{Parser, ParserError, ParserOptions, Result}; impl DeclarationParser for FontPaletteValuesDeclarationParser { type Declaration = FontPaletteValuesProperty; @@ -294,52 +292,10 @@ const _: () = { impl AtRuleParser for FontPaletteValuesDeclarationParser { type Prelude = (); type AtRule = FontPaletteValuesProperty; - - fn parse_prelude( - _this: &mut Self, - name: &[u8], - input: &mut Parser, - ) -> Result { - Err( - input.new_error(BasicParseErrorKind::at_rule_invalid(std::ptr::from_ref::< - [u8], - >(name))), - ) - } - - fn parse_block( - _this: &mut Self, - _prelude: Self::Prelude, - _start: &ParserState, - input: &mut Parser, - ) -> Result { - Err(input.new_error(BasicParseErrorKind::at_rule_body_invalid)) - } - - fn rule_without_block( - _this: &mut Self, - _prelude: Self::Prelude, - _start: &ParserState, - ) -> Maybe { - Err(()) - } } impl QualifiedRuleParser for FontPaletteValuesDeclarationParser { type Prelude = (); type QualifiedRule = FontPaletteValuesProperty; - - fn parse_prelude(_this: &mut Self, input: &mut Parser) -> Result { - Err(input.new_error(BasicParseErrorKind::qualified_rule_invalid)) - } - - fn parse_block( - _this: &mut Self, - _prelude: Self::Prelude, - _start: &ParserState, - input: &mut Parser, - ) -> Result { - Err(input.new_error(BasicParseErrorKind::qualified_rule_invalid)) - } } }; diff --git a/src/css/rules/keyframes.rs b/src/css/rules/keyframes.rs index 06751a4ee891..06ca23fb954b 100644 --- a/src/css/rules/keyframes.rs +++ b/src/css/rules/keyframes.rs @@ -325,7 +325,7 @@ const _: () = { use css::css_parser::{ AtRuleParser, DeclarationParser, QualifiedRuleParser, RuleBodyItemParser, }; - use css::{BasicParseErrorKind, Maybe, Parser, ParserOptions, ParserState, Result}; + use css::{BasicParseErrorKind, Parser, ParserOptions, ParserState, Result}; impl DeclarationParser for KeyframesListParser { type Declaration = Keyframe; @@ -357,35 +357,6 @@ const _: () = { impl AtRuleParser for KeyframesListParser { type Prelude = (); type AtRule = Keyframe; - - fn parse_prelude( - _this: &mut Self, - name: &[u8], - input: &mut Parser, - ) -> Result { - Err( - input.new_error(BasicParseErrorKind::at_rule_invalid(std::ptr::from_ref::< - [u8], - >(name))), - ) - } - - fn parse_block( - _this: &mut Self, - _prelude: Self::Prelude, - _start: &ParserState, - input: &mut Parser, - ) -> Result { - Err(input.new_error(BasicParseErrorKind::at_rule_body_invalid)) - } - - fn rule_without_block( - _this: &mut Self, - _prelude: Self::Prelude, - _start: &ParserState, - ) -> Maybe { - Err(()) - } } impl QualifiedRuleParser for KeyframesListParser { diff --git a/src/css/rules/page.rs b/src/css/rules/page.rs index 1e1e059661a5..8251c4e88fec 100644 --- a/src/css/rules/page.rs +++ b/src/css/rules/page.rs @@ -310,7 +310,7 @@ const _: () = { use css::css_parser::{ AtRuleParser, DeclarationParser, QualifiedRuleParser, RuleBodyItemParser, }; - use css::{BasicParseErrorKind, Maybe, Parser, ParserError, ParserState, Result}; + use css::{Parser, ParserError, ParserState, Result}; impl<'a> DeclarationParser for PageRuleParser<'a> { type Declaration = (); @@ -377,31 +377,10 @@ const _: () = { }); Ok(()) } - - fn rule_without_block( - _this: &mut Self, - _prelude: Self::Prelude, - _start: &ParserState, - ) -> Maybe { - Err(()) - } } impl<'a> QualifiedRuleParser for PageRuleParser<'a> { type Prelude = (); type QualifiedRule = (); - - fn parse_prelude(_this: &mut Self, input: &mut Parser) -> Result { - Err(input.new_error(BasicParseErrorKind::qualified_rule_invalid)) - } - - fn parse_block( - _this: &mut Self, - _prelude: Self::Prelude, - _start: &ParserState, - input: &mut Parser, - ) -> Result { - Err(input.new_error(BasicParseErrorKind::qualified_rule_invalid)) - } } }; diff --git a/src/css/rules/property.rs b/src/css/rules/property.rs index 4c4f105d40be..30187e9ad9b8 100644 --- a/src/css/rules/property.rs +++ b/src/css/rules/property.rs @@ -166,7 +166,7 @@ const _: () = { use css::css_parser::{ AtRuleParser, DeclarationParser, QualifiedRuleParser, RuleBodyItemParser, }; - use css::{BasicParseErrorKind, Maybe, Parser, ParserError, ParserState, Result}; + use css::{Parser, ParserError, Result}; impl DeclarationParser for PropertyRuleDeclarationParser { type Declaration = (); @@ -216,52 +216,10 @@ const _: () = { impl AtRuleParser for PropertyRuleDeclarationParser { type Prelude = (); type AtRule = (); - - fn parse_prelude( - _this: &mut Self, - name: &[u8], - input: &mut Parser, - ) -> Result { - Err( - input.new_error(BasicParseErrorKind::at_rule_invalid(std::ptr::from_ref::< - [u8], - >(name))), - ) - } - - fn parse_block( - _this: &mut Self, - _prelude: Self::Prelude, - _start: &ParserState, - input: &mut Parser, - ) -> Result { - Err(input.new_error(BasicParseErrorKind::at_rule_body_invalid)) - } - - fn rule_without_block( - _this: &mut Self, - _prelude: Self::Prelude, - _start: &ParserState, - ) -> Maybe { - Err(()) - } } impl QualifiedRuleParser for PropertyRuleDeclarationParser { type Prelude = (); type QualifiedRule = (); - - fn parse_prelude(_this: &mut Self, input: &mut Parser) -> Result { - Err(input.new_error(BasicParseErrorKind::qualified_rule_invalid)) - } - - fn parse_block( - _this: &mut Self, - _prelude: Self::Prelude, - _start: &ParserState, - input: &mut Parser, - ) -> Result { - Err(input.new_error(BasicParseErrorKind::qualified_rule_invalid)) - } } }; diff --git a/src/errno/windows_errno.rs b/src/errno/windows_errno.rs index 252ce56bf163..fe1d3cc71efa 100644 --- a/src/errno/windows_errno.rs +++ b/src/errno/windows_errno.rs @@ -13,20 +13,21 @@ pub use self::windows::{NTSTATUS, Win32Error, Win32ErrorExt}; use bun_libuv_sys as uv; // ────────────────────────────────────────────────────────────────────────── -// UV_* errno X-macro +// errno X-macros // -// Single source of truth for the 86 UV_* variants that form the tail of BOTH -// `enum E` and `enum SystemErrno`. Both enum tails are -// driven from this one list so they cannot drift. (The UV_*→E* fold-down -// lives in `bun_libuv_sys::uv_err_to_e_discriminant`.) +// BOTH `enum E` and `enum SystemErrno` are generated from the same two lists +// so they cannot drift: +// • `for_each_linux_errno!` — the dense Linux-numbered `0..=137` head +// • `for_each_uv_errno!` — the 86 UV_* variants forming the tail +// (the UV_*→E* fold-down lives in `bun_libuv_sys::uv_err_to_e_discriminant`) // -// Entry shape: +// UV entry shape: // [UV_X => EX] — UV_X has a non-UV_ counterpart `SystemErrno::EX` // [UV_X] — no counterpart (EAI_* resolver codes, UNKNOWN, ERRNO_MAX) // // ORDER IS LOAD-BEARING: `enum_map::Enum` derives ordinals from declaration // order, and `SystemErrno::to_e` transmutes by discriminant, so the two enums -// MUST stay in lockstep. Editing this list updates both atomically. +// MUST stay in lockstep. Editing these lists updates both atomically. // ────────────────────────────────────────────────────────────────────────── /// X-macro: invokes `$cb! { $($pre)* @uv [UV_X => EX] [UV_Y] … }`. @@ -65,17 +66,117 @@ macro_rules! for_each_uv_errno { }; } -/// Callback: emit `$pre` enum decl with the UV_* tail appended verbatim. -macro_rules! __errno_enum_with_uv_tail { +/// X-macro: invokes `$cb! { $($pre)* @linux [PERM EPERM = 1] … }`. +/// +/// Row shape: `[ = N]` — `enum E` uses the bare spelling +/// (`PERM`), `enum SystemErrno` the prefixed one (`EPERM`). Optional leading +/// `#[meta]` attributes apply to the bare variant only. +macro_rules! for_each_linux_errno { + ($cb:ident { $($pre:tt)* }) => { + $cb! { $($pre)* @linux + [SUCCESS SUCCESS = 0] [PERM EPERM = 1] [NOENT ENOENT = 2] [SRCH ESRCH = 3] + [INTR EINTR = 4] [IO EIO = 5] [NXIO ENXIO = 6] + // Rust identifiers cannot start with a digit. + [#[strum(serialize = "2BIG")] _2BIG E2BIG = 7] + [NOEXEC ENOEXEC = 8] [BADF EBADF = 9] [CHILD ECHILD = 10] [AGAIN EAGAIN = 11] + [NOMEM ENOMEM = 12] [ACCES EACCES = 13] [FAULT EFAULT = 14] [NOTBLK ENOTBLK = 15] + [BUSY EBUSY = 16] [EXIST EEXIST = 17] [XDEV EXDEV = 18] [NODEV ENODEV = 19] + [NOTDIR ENOTDIR = 20] [ISDIR EISDIR = 21] [INVAL EINVAL = 22] [NFILE ENFILE = 23] + [MFILE EMFILE = 24] [NOTTY ENOTTY = 25] [TXTBSY ETXTBSY = 26] [FBIG EFBIG = 27] + [NOSPC ENOSPC = 28] [SPIPE ESPIPE = 29] [ROFS EROFS = 30] [MLINK EMLINK = 31] + [PIPE EPIPE = 32] [DOM EDOM = 33] [RANGE ERANGE = 34] [DEADLK EDEADLK = 35] + [NAMETOOLONG ENAMETOOLONG = 36] [NOLCK ENOLCK = 37] [NOSYS ENOSYS = 38] + [NOTEMPTY ENOTEMPTY = 39] [LOOP ELOOP = 40] [WOULDBLOCK EWOULDBLOCK = 41] + [NOMSG ENOMSG = 42] [IDRM EIDRM = 43] [CHRNG ECHRNG = 44] [L2NSYNC EL2NSYNC = 45] + [L3HLT EL3HLT = 46] [L3RST EL3RST = 47] [LNRNG ELNRNG = 48] [UNATCH EUNATCH = 49] + [NOCSI ENOCSI = 50] [L2HLT EL2HLT = 51] [BADE EBADE = 52] [BADR EBADR = 53] + [XFULL EXFULL = 54] [NOANO ENOANO = 55] [BADRQC EBADRQC = 56] [BADSLT EBADSLT = 57] + [DEADLOCK EDEADLOCK = 58] [BFONT EBFONT = 59] [NOSTR ENOSTR = 60] + [NODATA ENODATA = 61] [TIME ETIME = 62] [NOSR ENOSR = 63] [NONET ENONET = 64] + [NOPKG ENOPKG = 65] [REMOTE EREMOTE = 66] [NOLINK ENOLINK = 67] [ADV EADV = 68] + [SRMNT ESRMNT = 69] [COMM ECOMM = 70] [PROTO EPROTO = 71] [MULTIHOP EMULTIHOP = 72] + [DOTDOT EDOTDOT = 73] [BADMSG EBADMSG = 74] [OVERFLOW EOVERFLOW = 75] + [NOTUNIQ ENOTUNIQ = 76] [BADFD EBADFD = 77] [REMCHG EREMCHG = 78] + [LIBACC ELIBACC = 79] [LIBBAD ELIBBAD = 80] [LIBSCN ELIBSCN = 81] + [LIBMAX ELIBMAX = 82] [LIBEXEC ELIBEXEC = 83] [ILSEQ EILSEQ = 84] + [RESTART ERESTART = 85] [STRPIPE ESTRPIPE = 86] [USERS EUSERS = 87] + [NOTSOCK ENOTSOCK = 88] [DESTADDRREQ EDESTADDRREQ = 89] [MSGSIZE EMSGSIZE = 90] + [PROTOTYPE EPROTOTYPE = 91] [NOPROTOOPT ENOPROTOOPT = 92] + [PROTONOSUPPORT EPROTONOSUPPORT = 93] [SOCKTNOSUPPORT ESOCKTNOSUPPORT = 94] + // On Linux EOPNOTSUPP is the real value, but it's ~the same and is + // incompatible across operating systems: + // https://lists.gnu.org/archive/html/bug-glibc/2002-08/msg00017.html + [NOTSUP ENOTSUP = 95] + [PFNOSUPPORT EPFNOSUPPORT = 96] [AFNOSUPPORT EAFNOSUPPORT = 97] + [ADDRINUSE EADDRINUSE = 98] [ADDRNOTAVAIL EADDRNOTAVAIL = 99] + [NETDOWN ENETDOWN = 100] [NETUNREACH ENETUNREACH = 101] [NETRESET ENETRESET = 102] + [CONNABORTED ECONNABORTED = 103] [CONNRESET ECONNRESET = 104] + [NOBUFS ENOBUFS = 105] [ISCONN EISCONN = 106] [NOTCONN ENOTCONN = 107] + [SHUTDOWN ESHUTDOWN = 108] [TOOMANYREFS ETOOMANYREFS = 109] + [TIMEDOUT ETIMEDOUT = 110] [CONNREFUSED ECONNREFUSED = 111] + [HOSTDOWN EHOSTDOWN = 112] [HOSTUNREACH EHOSTUNREACH = 113] + [ALREADY EALREADY = 114] [INPROGRESS EINPROGRESS = 115] [STALE ESTALE = 116] + [UCLEAN EUCLEAN = 117] [NOTNAM ENOTNAM = 118] [NAVAIL ENAVAIL = 119] + [ISNAM EISNAM = 120] [REMOTEIO EREMOTEIO = 121] [DQUOT EDQUOT = 122] + [NOMEDIUM ENOMEDIUM = 123] [MEDIUMTYPE EMEDIUMTYPE = 124] + [CANCELED ECANCELED = 125] [NOKEY ENOKEY = 126] [KEYEXPIRED EKEYEXPIRED = 127] + [KEYREVOKED EKEYREVOKED = 128] [KEYREJECTED EKEYREJECTED = 129] + [OWNERDEAD EOWNERDEAD = 130] [NOTRECOVERABLE ENOTRECOVERABLE = 131] + [RFKILL ERFKILL = 132] [HWPOISON EHWPOISON = 133] + // 134..=137 are made-up / libuv-synthetic codes with no Linux number. + [UNKNOWN EUNKNOWN = 134] [CHARSET ECHARSET = 135] [EOF EOF = 136] + [FTYPE EFTYPE = 137] + } + }; +} + +/// Relay: forwards `$pre` (which already carries the `@linux` head) into +/// `for_each_uv_errno!` so `__errno_enum!` sees both row lists at once. +macro_rules! __errno_enum_add_uv_tail { + ($($toks:tt)*) => { + for_each_uv_errno! { __errno_enum { $($toks)* } } + }; +} + +/// Emits one `#[repr(u16)]` errno enum from the Linux head + UV_* tail. +/// `@bare` picks each head row's first ident (`E`); `@prefixed` picks the +/// second (`SystemErrno`) and drops the bare-only attributes. +macro_rules! __errno_enum { ( - $(#[$m:meta])* $vis:vis enum $name:ident { $($head:tt)* } - @uv $( [ $uv:ident $(=> $sys:ident)? ] )* + @bare $vis:vis enum $name:ident + @linux $( [ $(#[$bm:meta])* $bare:ident $sys:ident = $val:literal ] )* + @uv $( [ $uv:ident $(=> $uv_sys:ident)? ] )* ) => { - $(#[$m])* - $vis enum $name { - $($head)* + __errno_enum! { @emit $vis enum $name { + $( $(#[$bm])* $bare = $val, )* $( $uv = (-uv::$uv) as u16, )* - } + } } + }; + ( + @prefixed $vis:vis enum $name:ident + @linux $( [ $(#[$bm:meta])* $bare:ident $sys:ident = $val:literal ] )* + @uv $( [ $uv:ident $(=> $uv_sys:ident)? ] )* + ) => { + __errno_enum! { @emit $vis enum $name { + $( $sys = $val, )* + $( $uv = (-uv::$uv) as u16, )* + } } + }; + (@emit $vis:vis enum $name:ident { $($body:tt)* }) => { + #[repr(u16)] + #[derive( + Copy, + Clone, + Eq, + PartialEq, + Hash, + Debug, + strum::IntoStaticStr, + strum::EnumString, + strum::FromRepr, + enum_map::Enum, + )] + $vis enum $name { $($body)* } }; } @@ -83,163 +184,7 @@ macro_rules! __errno_enum_with_uv_tail { // E // ────────────────────────────────────────────────────────────────────────── -for_each_uv_errno! { __errno_enum_with_uv_tail { -#[repr(u16)] -#[derive( - Copy, - Clone, - Eq, - PartialEq, - Hash, - Debug, - strum::IntoStaticStr, - strum::EnumString, - strum::FromRepr, - enum_map::Enum, -)] -pub enum E { - SUCCESS = 0, - PERM = 1, - NOENT = 2, - SRCH = 3, - INTR = 4, - IO = 5, - NXIO = 6, - // Rust identifiers cannot start with a digit. - #[strum(serialize = "2BIG")] - _2BIG = 7, - NOEXEC = 8, - BADF = 9, - CHILD = 10, - AGAIN = 11, - NOMEM = 12, - ACCES = 13, - FAULT = 14, - NOTBLK = 15, - BUSY = 16, - EXIST = 17, - XDEV = 18, - NODEV = 19, - NOTDIR = 20, - ISDIR = 21, - INVAL = 22, - NFILE = 23, - MFILE = 24, - NOTTY = 25, - TXTBSY = 26, - FBIG = 27, - NOSPC = 28, - SPIPE = 29, - ROFS = 30, - MLINK = 31, - PIPE = 32, - DOM = 33, - RANGE = 34, - DEADLK = 35, - NAMETOOLONG = 36, - NOLCK = 37, - NOSYS = 38, - NOTEMPTY = 39, - LOOP = 40, - WOULDBLOCK = 41, - NOMSG = 42, - IDRM = 43, - CHRNG = 44, - L2NSYNC = 45, - L3HLT = 46, - L3RST = 47, - LNRNG = 48, - UNATCH = 49, - NOCSI = 50, - L2HLT = 51, - BADE = 52, - BADR = 53, - XFULL = 54, - NOANO = 55, - BADRQC = 56, - BADSLT = 57, - DEADLOCK = 58, - BFONT = 59, - NOSTR = 60, - NODATA = 61, - TIME = 62, - NOSR = 63, - NONET = 64, - NOPKG = 65, - REMOTE = 66, - NOLINK = 67, - ADV = 68, - SRMNT = 69, - COMM = 70, - PROTO = 71, - MULTIHOP = 72, - DOTDOT = 73, - BADMSG = 74, - OVERFLOW = 75, - NOTUNIQ = 76, - BADFD = 77, - REMCHG = 78, - LIBACC = 79, - LIBBAD = 80, - LIBSCN = 81, - LIBMAX = 82, - LIBEXEC = 83, - ILSEQ = 84, - RESTART = 85, - STRPIPE = 86, - USERS = 87, - NOTSOCK = 88, - DESTADDRREQ = 89, - MSGSIZE = 90, - PROTOTYPE = 91, - NOPROTOOPT = 92, - PROTONOSUPPORT = 93, - SOCKTNOSUPPORT = 94, - NOTSUP = 95, - PFNOSUPPORT = 96, - AFNOSUPPORT = 97, - ADDRINUSE = 98, - ADDRNOTAVAIL = 99, - NETDOWN = 100, - NETUNREACH = 101, - NETRESET = 102, - CONNABORTED = 103, - CONNRESET = 104, - NOBUFS = 105, - ISCONN = 106, - NOTCONN = 107, - SHUTDOWN = 108, - TOOMANYREFS = 109, - TIMEDOUT = 110, - CONNREFUSED = 111, - HOSTDOWN = 112, - HOSTUNREACH = 113, - ALREADY = 114, - INPROGRESS = 115, - STALE = 116, - UCLEAN = 117, - NOTNAM = 118, - NAVAIL = 119, - ISNAM = 120, - REMOTEIO = 121, - DQUOT = 122, - NOMEDIUM = 123, - MEDIUMTYPE = 124, - CANCELED = 125, - NOKEY = 126, - KEYEXPIRED = 127, - KEYREVOKED = 128, - KEYREJECTED = 129, - OWNERDEAD = 130, - NOTRECOVERABLE = 131, - RFKILL = 132, - HWPOISON = 133, - UNKNOWN = 134, - CHARSET = 135, - EOF = 136, - FTYPE = 137, -} -}} // ← UV_* tail appended by `for_each_uv_errno!` +for_each_linux_errno! { __errno_enum_add_uv_tail { @bare pub enum E } } impl E { #[inline] @@ -398,165 +343,7 @@ pub fn get_errno(_rc: T) -> E { // SystemErrno // ────────────────────────────────────────────────────────────────────────── -for_each_uv_errno! { __errno_enum_with_uv_tail { -#[repr(u16)] -#[derive( - Copy, - Clone, - Eq, - PartialEq, - Hash, - Debug, - strum::IntoStaticStr, - strum::EnumString, - strum::FromRepr, - enum_map::Enum, -)] -pub enum SystemErrno { - SUCCESS = 0, - EPERM = 1, - ENOENT = 2, - ESRCH = 3, - EINTR = 4, - EIO = 5, - ENXIO = 6, - E2BIG = 7, - ENOEXEC = 8, - EBADF = 9, - ECHILD = 10, - EAGAIN = 11, - ENOMEM = 12, - EACCES = 13, - EFAULT = 14, - ENOTBLK = 15, - EBUSY = 16, - EEXIST = 17, - EXDEV = 18, - ENODEV = 19, - ENOTDIR = 20, - EISDIR = 21, - EINVAL = 22, - ENFILE = 23, - EMFILE = 24, - ENOTTY = 25, - ETXTBSY = 26, - EFBIG = 27, - ENOSPC = 28, - ESPIPE = 29, - EROFS = 30, - EMLINK = 31, - EPIPE = 32, - EDOM = 33, - ERANGE = 34, - EDEADLK = 35, - ENAMETOOLONG = 36, - ENOLCK = 37, - ENOSYS = 38, - ENOTEMPTY = 39, - ELOOP = 40, - EWOULDBLOCK = 41, - ENOMSG = 42, - EIDRM = 43, - ECHRNG = 44, - EL2NSYNC = 45, - EL3HLT = 46, - EL3RST = 47, - ELNRNG = 48, - EUNATCH = 49, - ENOCSI = 50, - EL2HLT = 51, - EBADE = 52, - EBADR = 53, - EXFULL = 54, - ENOANO = 55, - EBADRQC = 56, - EBADSLT = 57, - EDEADLOCK = 58, - EBFONT = 59, - ENOSTR = 60, - ENODATA = 61, - ETIME = 62, - ENOSR = 63, - ENONET = 64, - ENOPKG = 65, - EREMOTE = 66, - ENOLINK = 67, - EADV = 68, - ESRMNT = 69, - ECOMM = 70, - EPROTO = 71, - EMULTIHOP = 72, - EDOTDOT = 73, - EBADMSG = 74, - EOVERFLOW = 75, - ENOTUNIQ = 76, - EBADFD = 77, - EREMCHG = 78, - ELIBACC = 79, - ELIBBAD = 80, - ELIBSCN = 81, - ELIBMAX = 82, - ELIBEXEC = 83, - EILSEQ = 84, - ERESTART = 85, - ESTRPIPE = 86, - EUSERS = 87, - ENOTSOCK = 88, - EDESTADDRREQ = 89, - EMSGSIZE = 90, - EPROTOTYPE = 91, - ENOPROTOOPT = 92, - EPROTONOSUPPORT = 93, - ESOCKTNOSUPPORT = 94, - /// For Linux, EOPNOTSUPP is the real value - /// but it's ~the same and is incompatible across operating systems - /// https://lists.gnu.org/archive/html/bug-glibc/2002-08/msg00017.html - ENOTSUP = 95, - EPFNOSUPPORT = 96, - EAFNOSUPPORT = 97, - EADDRINUSE = 98, - EADDRNOTAVAIL = 99, - ENETDOWN = 100, - ENETUNREACH = 101, - ENETRESET = 102, - ECONNABORTED = 103, - ECONNRESET = 104, - ENOBUFS = 105, - EISCONN = 106, - ENOTCONN = 107, - ESHUTDOWN = 108, - ETOOMANYREFS = 109, - ETIMEDOUT = 110, - ECONNREFUSED = 111, - EHOSTDOWN = 112, - EHOSTUNREACH = 113, - EALREADY = 114, - EINPROGRESS = 115, - ESTALE = 116, - EUCLEAN = 117, - ENOTNAM = 118, - ENAVAIL = 119, - EISNAM = 120, - EREMOTEIO = 121, - EDQUOT = 122, - ENOMEDIUM = 123, - EMEDIUMTYPE = 124, - ECANCELED = 125, - ENOKEY = 126, - EKEYEXPIRED = 127, - EKEYREVOKED = 128, - EKEYREJECTED = 129, - EOWNERDEAD = 130, - ENOTRECOVERABLE = 131, - ERFKILL = 132, - EHWPOISON = 133, - // made up erropr - EUNKNOWN = 134, - ECHARSET = 135, - EOF = 136, - EFTYPE = 137, -} -}} // ← UV_* tail appended by `for_each_uv_errno!` +for_each_linux_errno! { __errno_enum_add_uv_tail { @prefixed pub enum SystemErrno } } /// Type-dispatch shim for `SystemErrno::init`. /// Covers every concrete type the codebase actually passes — `i64` (shared diff --git a/src/http/h2_client/dispatch.rs b/src/http/h2_client/dispatch.rs index e0b19b2a4bc8..f0d3e68523b7 100644 --- a/src/http/h2_client/dispatch.rs +++ b/src/http/h2_client/dispatch.rs @@ -734,31 +734,9 @@ pub(crate) fn strip_padding(payload: &[u8]) -> Option<&[u8]> { /// hop-by-hop fields. Names from lshpack are already lowercase for table /// hits but a literal can carry anything. pub(crate) fn is_malformed_response_field(name: &[u8]) -> bool { - if name.is_empty() { + if name.is_empty() || !name.iter().all(|&c| wire::is_lower_tchar(c)) { return true; } - for &c in name { - match c { - b'a'..=b'z' - | b'0'..=b'9' - | b'!' - | b'#' - | b'$' - | b'%' - | b'&' - | b'\'' - | b'*' - | b'+' - | b'-' - | b'.' - | b'^' - | b'_' - | b'`' - | b'|' - | b'~' => {} - _ => return true, - } - } matches!( name, b"connection" @@ -770,13 +748,7 @@ pub(crate) fn is_malformed_response_field(name: &[u8]) -> bool { ) } -/// RFC 9113 §8.2.1: a field value MUST NOT contain NUL (0x00), LF (0x0a), or -/// CR (0x0d). HPACK is length-prefixed so these would otherwise pass through -/// verbatim, breaking the no-CR/LF invariant the HTTP/1.1 parser provides and -/// enabling header injection when values are forwarded downstream. -pub fn is_malformed_response_value(value: &[u8]) -> bool { - value.iter().any(|&c| c == 0 || c == b'\r' || c == b'\n') -} +pub use wire::is_malformed_field_value as is_malformed_response_value; pub fn error_code_for(err: bun_core::Error) -> wire::ErrorCode { // bun_core::Error is a NonZeroU16 interned tag; `err!()` yields diff --git a/src/http/lib.rs b/src/http/lib.rs index 82ba943c9e12..d921470a420a 100644 --- a/src/http/lib.rs +++ b/src/http/lib.rs @@ -3630,49 +3630,7 @@ impl<'a> HTTPClient<'a> { if self.flags.protocol != Protocol::Http1_1 { return self.send_progress_update_multiplexed(); } - // reshaped for borrowck — `to_result()` returns an - // `HTTPClientResult<'_>` whose lifetime is tied to `&mut self` (via the - // `body: &mut MutableString` borrow). Holding that result across the - // `is_done` mutations below would require a second live `&mut Self`, - // which PORTING.md §Forbidden flags as aliased `&mut`. Instead: - // snapshot every owned/Copy field out of the result, drop it, mutate - // `self` directly, then rebuild a fresh `HTTPClientResult` for the - // callback from the snapshotted fields + the restored body. - let body = self.state.body_out_str; - // Snapshot the body buffer's CONTENTS by value so that `state.reset()` - // — which calls `body.reset()` and clears the list — doesn't deliver - // an empty body when `is_done`. Restored below before the callback. - let body_snapshot = body_out::take_list(body); - let callback = self.result_callback; - - let ( - has_more, - redirected, - can_stream, - is_http2, - fail, - metadata, - body_size, - certificate_info, - ) = { - let r = self.to_result(); - ( - r.has_more, - r.redirected, - r.can_stream, - r.is_http2, - r.fail, - r.metadata, - r.body_size, - r.certificate_info, - ) - }; // r (and its &mut borrow of self) dropped here - let is_done = !has_more; - - bun_core::scoped_log!(fetch, "progressUpdate {}", is_done); - - if is_done { - self.unregister_abort_tracker(); + self.send_progress_update_inner(|this| { // is_done is response-driven. A server can reply early (HTTP 413) // with keep-alive while request_stage is still .proxy_body or the // tunnel still has buffered encrypted writes. Pooling that tunnel @@ -3685,8 +3643,8 @@ impl<'a> HTTPClient<'a> { // ends on inner-TLS close; ProxyTunnel.onClose fires but the outer // socket is still alive. Pooling that dead wrapper would hang the // next request (proxy.write() → error.ConnectionClosed, swallowed). - let tunnel_poolable = if let Some(t) = self.proxy_tunnel.as_deref() { - self.state.request_stage == RequestStage::Done + let tunnel_poolable = if let Some(t) = this.proxy_tunnel.as_deref() { + this.state.request_stage == RequestStage::Done && t.write_buffer.is_empty() && t.wrapper .as_ref() @@ -3710,12 +3668,12 @@ impl<'a> HTTPClient<'a> { // so for byte-buffer bodies check the unsent slice instead. // Stream/Sendfile are left as-is (they don't track an // unsent slice here). - let request_side_drained = match &self.state.original_request_body { - HTTPRequestBody::Bytes(_) => self.state.request_body.is_empty(), + let request_side_drained = match &this.state.original_request_body { + HTTPRequestBody::Bytes(_) => this.state.request_body.is_empty(), _ => true, }; - if self.is_keep_alive_possible() + if this.is_keep_alive_possible() && !socket.is_closed_or_has_error() && tunnel_poolable && request_side_drained @@ -3724,9 +3682,9 @@ impl<'a> HTTPClient<'a> { // Hand the client's strong ref straight to the pool: `release_socket` // either stores this `RefPtr` in the parked `PooledSocket` or // dereffs it if pooling fails. - let tunnel = self.proxy_tunnel.take(); + let tunnel = this.proxy_tunnel.take(); if let Some(t) = &tunnel { - proxy_tunnel::raw_as_mut(t.as_ptr()).detach_owner(&*self); + proxy_tunnel::raw_as_mut(t.as_ptr()).detach_owner(&*this); } let had_tunnel = tunnel.is_some(); // target_hostname = url.hostname (the CONNECT TCP target at @@ -3735,62 +3693,38 @@ impl<'a> HTTPClient<'a> { // they're distinct values when a Host header override is set. Self::ssl_ctx_mut(ctx).release_socket( socket, - self.flags.did_have_handshaking_error && !self.flags.reject_unauthorized, - self.flags.reject_unauthorized, - self.connected_url.hostname, - self.connected_url.get_port_auto(), - self.tls_props.as_ref(), + this.flags.did_have_handshaking_error && !this.flags.reject_unauthorized, + this.flags.reject_unauthorized, + this.connected_url.hostname, + this.connected_url.get_port_auto(), + this.tls_props.as_ref(), tunnel, - if had_tunnel { self.url.hostname } else { b"" }, + if had_tunnel { this.url.hostname } else { b"" }, if had_tunnel { - self.url.get_port_auto() + this.url.get_port_auto() } else { 0 }, - if had_tunnel || (IS_SSL && self.http_proxy.is_none()) { + if had_tunnel || (IS_SSL && this.http_proxy.is_none()) { // Direct TLS: the handshake verified the peer against // the Host-header override (get_tls_hostname), so the // override hash must be part of the pool key. Matches // the lookup in HTTPContext::connect. - self.proxy_auth_hash() + this.proxy_auth_hash() } else { 0 }, None, ); } else { - if self.proxy_tunnel.is_some() { + if this.proxy_tunnel.is_some() { bun_core::scoped_log!(fetch, "close the tunnel"); - self.close_proxy_tunnel(true); + this.close_proxy_tunnel(true); } GenHttpContext::::close_socket(socket); } - - self.state.reset(); - self.state.response_stage = ResponseStage::Done; - self.state.request_stage = RequestStage::Done; - self.state.stage = Stage::Done; - self.flags.proxy_tunneling = false; bun_core::scoped_log!(fetch, "done"); - } - - // Restore the body bytes that `state.reset()` cleared. - body_out::restore_list(body, body_snapshot); - let async_http = self.parent_async_http(); - // Rebuild the result from snapshotted fields now that all `&mut self` - // mutations are finished — no aliased borrows remain. - let result = HTTPClientResult { - body: body_out::opt_mut(body), - has_more, - redirected, - can_stream, - is_http2, - fail, - metadata, - body_size, - certificate_info, - }; - callback.run(async_http, result); + }); if PRINT_EVERY != 0 { let i = PRINT_EVERY_I.fetch_add(1, Ordering::Relaxed) + 1; @@ -3808,14 +3742,26 @@ impl<'a> HTTPClient<'a> { /// transport, so there is no `ctx`/`socket` to hand back to the pool here. fn send_progress_update_multiplexed(&mut self) { debug_assert!(self.flags.protocol != Protocol::Http1_1); - // reshaped for borrowck — `to_result()` ties `result`'s - // lifetime to `&mut self`, so holding it across the `is_done` mutations - // would require a second live `&mut Self` (aliased UB). Instead snapshot - // every owned/Copy field out of the result, drop it, mutate `self` - // directly, then rebuild a fresh `HTTPClientResult` for the callback. - // See send_progress_update_without_stage_check for the same pattern. + self.send_progress_update_inner(|_| {}); + } + + /// Shared tail of the two progress-update paths: snapshot the result, + /// run `release_transport` once the response is done (the HTTP/1.1 path + /// hands its socket back to the pool there), reset state, and deliver the + /// result to the callback. + fn send_progress_update_inner(&mut self, release_transport: impl FnOnce(&mut Self)) { + // reshaped for borrowck — `to_result()` returns an + // `HTTPClientResult<'_>` whose lifetime is tied to `&mut self` (via the + // `body: &mut MutableString` borrow). Holding that result across the + // `is_done` mutations below would require a second live `&mut Self`, + // which PORTING.md §Forbidden flags as aliased `&mut`. Instead: + // snapshot every owned/Copy field out of the result, drop it, mutate + // `self` directly, then rebuild a fresh `HTTPClientResult` for the + // callback from the snapshotted fields + the restored body. let body = self.state.body_out_str; - // Snapshot the body buffer's CONTENTS by value; restored below. + // Snapshot the body buffer's CONTENTS by value so that `state.reset()` + // — which calls `body.reset()` and clears the list — doesn't deliver + // an empty body when `is_done`. Restored below before the callback. let body_snapshot = body_out::take_list(body); let callback = self.result_callback; @@ -3842,15 +3788,19 @@ impl<'a> HTTPClient<'a> { ) }; // r (and its &mut borrow of self) dropped here let is_done = !has_more; + bun_core::scoped_log!(fetch, "progressUpdate {}", is_done); + if is_done { self.unregister_abort_tracker(); + release_transport(self); self.state.reset(); self.state.response_stage = ResponseStage::Done; self.state.request_stage = RequestStage::Done; self.state.stage = Stage::Done; self.flags.proxy_tunneling = false; } + // Restore the body bytes that `state.reset()` cleared. body_out::restore_list(body, body_snapshot); let async_http = self.parent_async_http(); @@ -4364,6 +4314,44 @@ impl<'a> HTTPClient<'a> { } } + /// Shared tail of the `Location`-header arms in + /// `handle_response_metadata`: parse the rebuilt absolute href, compare + /// origins against the current URL, then swap the href into + /// `self.redirect`. Returns whether the redirect target is same-origin. + fn apply_redirect_url(&mut self, new_href: Vec) -> bool { + // SAFETY: self-borrow — `new_href` is moved into `self.redirect` + // below, which lives as long as `self` (≥ `'a`). + let new_url: URL<'a> = unsafe { URL::parse(&new_href).erase_lifetime() }; + let is_same_origin = strings::eql_case_insensitive_ascii( + strings::without_trailing_slash(new_url.origin), + strings::without_trailing_slash(self.url.origin), + true, + ); + self.url = new_url; + // connected_url still borrows from the previous hop's buffer until + // doRedirect releases the socket, so park it in prev_redirect for + // doRedirect to free instead of leaking it. + debug_assert!(self.prev_redirect.is_empty()); + self.prev_redirect = core::mem::replace(&mut self.redirect, new_href); + is_same_origin + } + + /// Normalize a fully-rebuilt redirect URL through the WHATWG parser and + /// apply it via [`Self::apply_redirect_url`]. + fn normalize_and_apply_redirect_url( + &mut self, + mut string_builder: StringBuilder, + ) -> Result { + debug_assert!(string_builder.cap == string_builder.len); + let input = BunString::borrow_utf8(string_builder.allocated_slice()); + let normalized_url = OwnedString::new(bun_url::href_from_string(&input)); + if normalized_url.tag() == BunStringTag::Dead { + // URL__getHref failed, dont pass dead tagged string to toOwnedSlice. + return Err(err!(RedirectURLInvalid)); + } + Ok(self.apply_redirect_url(normalized_url.to_owned_slice())) + } + pub fn handle_response_metadata( &mut self, response: &mut picohttp::Response, @@ -4658,36 +4646,8 @@ impl<'a> HTTPClient<'a> { let _ = string_builder.append(location); - if cfg!(debug_assertions) { - debug_assert!(string_builder.cap == string_builder.len); - } - - let input = - BunString::borrow_utf8(string_builder.allocated_slice()); - let normalized_url = - OwnedString::new(bun_url::href_from_string(&input)); - if normalized_url.tag() == BunStringTag::Dead { - // URL__getHref failed, dont pass dead tagged string to toOwnedSlice. - return Err(err!(RedirectURLInvalid)); - } - let normalized_url_str = normalized_url.to_owned_slice(); - - // SAFETY: self-borrow — `normalized_url_str` is moved into - // `self.redirect` below, which lives as long as `self` (≥ `'a`). - let new_url: URL<'a> = - unsafe { URL::parse(&normalized_url_str).erase_lifetime() }; - is_same_origin = strings::eql_case_insensitive_ascii( - strings::without_trailing_slash(new_url.origin), - strings::without_trailing_slash(self.url.origin), - true, - ); - self.url = new_url; - // connected_url still borrows from the previous hop's buffer - // until doRedirect releases the socket, so park it in - // prev_redirect for doRedirect to free instead of leaking it. - debug_assert!(self.prev_redirect.is_empty()); - self.prev_redirect = - core::mem::replace(&mut self.redirect, normalized_url_str); + is_same_origin = + self.normalize_and_apply_redirect_url(string_builder)?; } else if location.starts_with(b"//") { let mut string_builder = StringBuilder::default(); @@ -4719,36 +4679,10 @@ impl<'a> HTTPClient<'a> { let _ = string_builder.append(location); - if cfg!(debug_assertions) { - debug_assert!(string_builder.cap == string_builder.len); - } - - let input = - BunString::borrow_utf8(string_builder.allocated_slice()); - let normalized_url = - OwnedString::new(bun_url::href_from_string(&input)); - if normalized_url.tag() == BunStringTag::Dead { - return Err(err!(RedirectURLInvalid)); - } - let normalized_url_str = normalized_url.to_owned_slice(); - - // SAFETY: self-borrow — `normalized_url_str` is moved into - // `self.redirect` below, which lives as long as `self` (≥ `'a`). - let new_url: URL<'a> = - unsafe { URL::parse(&normalized_url_str).erase_lifetime() }; - is_same_origin = strings::eql_case_insensitive_ascii( - strings::without_trailing_slash(new_url.origin), - strings::without_trailing_slash(self.url.origin), - true, - ); - self.url = new_url; - debug_assert!(self.prev_redirect.is_empty()); - self.prev_redirect = - core::mem::replace(&mut self.redirect, normalized_url_str); + is_same_origin = + self.normalize_and_apply_redirect_url(string_builder)?; } else { - let original_url = self.url.clone(); - - let base = BunString::borrow_utf8(original_url.href); + let base = BunString::borrow_utf8(self.url.href); let rel = BunString::borrow_utf8(location); let new_url_ = OwnedString::new(bun_url::join(&base, &rel)); @@ -4756,18 +4690,7 @@ impl<'a> HTTPClient<'a> { return Err(err!(InvalidRedirectURL)); } - let new_url = new_url_.to_owned_slice(); - // SAFETY: self-borrow — `new_url` is moved into `self.redirect` - // below, which lives as long as `self` (≥ `'a`). - self.url = unsafe { URL::parse(&new_url).erase_lifetime() }; - is_same_origin = strings::eql_case_insensitive_ascii( - strings::without_trailing_slash(self.url.origin), - strings::without_trailing_slash(original_url.origin), - true, - ); - debug_assert!(self.prev_redirect.is_empty()); - self.prev_redirect = - core::mem::replace(&mut self.redirect, new_url); + is_same_origin = self.apply_redirect_url(new_url_.to_owned_slice()); } } diff --git a/src/http_jsc/websocket_client.rs b/src/http_jsc/websocket_client.rs index 23aa399797ac..a1d6ecdaea2f 100644 --- a/src/http_jsc/websocket_client.rs +++ b/src/http_jsc/websocket_client.rs @@ -1717,16 +1717,19 @@ impl WebSocket { this.send_close_with_body(code, None, None, 0); } - pub extern "C" fn init( - outgoing: *mut CppWebSocket, - input_socket: *mut c_void, + /// Allocate a `WebSocket` with `ref_count == 1` and initialize deflate + /// if requested. The initial ref is the I/O-layer ref: the adopted-socket + /// ref in `init` (released by `handle_close`) or the tunnel-connection + /// ref in `init_with_tunnel` (released in `clear_data` when + /// `proxy_tunnel` is detached). The C++ ref paired with + /// `m_connectedWebSocket` is taken later in `finish_init`. + fn new_ws( global_this: &JSGlobalObject, - buffered_data: *mut u8, - buffered_data_len: usize, + outgoing: *mut CppWebSocket, + secure: Option<*mut SslCtx>, + proxy_tunnel: Option>, deflate_params: Option<&websocket_deflate::Params>, - secure_ptr: *mut c_void, - ) -> *mut c_void { - let tcp = input_socket.cast::(); + ) -> *mut Self { // outlives this call. let vm = global_this.bun_vm().as_mut(); let ws = bun_core::heap::into_raw(Box::new(WebSocket:: { @@ -1755,30 +1758,100 @@ impl WebSocket { initial_data_handler: None, // reshaped for borrowck — `vm.event_loop()` returns a // `&'static`-tied borrow that would lock `vm` for the rest of the - // fn; re-derive from `global_this` so `vm` stays usable below. + // fn; re-derive from `global_this` so `vm` stays usable for the + // deflate init below. // SAFETY: bun_vm() never returns null; event_loop ptr is live for VM lifetime. event_loop: global_this.bun_vm().event_loop_mut(), deflate: None, receiving_compressed: false, message_is_compressed: false, - secure: if secure_ptr.is_null() { - None - } else { - Some(secure_ptr.cast::()) - }, - proxy_tunnel: None, + secure, + proxy_tunnel, })); bun_core::scoped_log!(alloc, "new({}) = {:p}", Self::ALLOC_TYPE_NAME, ws); - // SAFETY: ws was just allocated via heap::alloc - let ws_ref = unsafe { &mut *ws }; if let Some(params) = deflate_params { - match WebSocketDeflate::init(*params, vm.rare_data()) { - Ok(deflate) => ws_ref.deflate = Some(deflate), - Err(_) => ws_ref.deflate = None, - } + // SAFETY: ws was just allocated via heap::alloc + unsafe { (*ws).deflate = WebSocketDeflate::init(*params, vm.rare_data()).ok() }; + } + + ws + } + + /// Shared tail of `init` / `init_with_tunnel`: preallocate the frame + /// buffers, ref the event loop, queue any buffered handshake data as a + /// microtask, and take the C++-side ref. + /// + /// # Safety + /// `ws` must be the live `heap::alloc` allocation returned by `new_ws`, + /// with no other `&`/`&mut` borrow of `*ws` live across this call. If + /// `buffered_data_len > 0`, `buffered_data` must be a mimalloc allocation + /// of that length whose ownership transfers to this call (extern-C + /// contract with the upgrade client). + unsafe fn finish_init( + ws: *mut Self, + global_this: &JSGlobalObject, + buffered: Option>, + ) -> *mut c_void { + // SAFETY: caller contract — `ws` is live with no other borrows. + let ws_ref = unsafe { &mut *ws }; + bun_core::handle_oom(ws_ref.send_buffer.ensure_total_capacity(2048)); + bun_core::handle_oom(ws_ref.receive_buffer.ensure_total_capacity(2048)); + ws_ref.poll_ref.r#ref(Self::vm_loop_ctx(global_this)); + + if let Some(buffered_slice) = buffered { + let initial_data = bun_core::heap::into_raw(Box::new(InitialDataHandler:: { + adopted: NonNull::new(ws), + slice: buffered_slice, + // We need to ref the outgoing websocket so that it doesn't get + // finalized before the initial data handler is called. + // SAFETY: `outgoing_websocket` (set by `new_ws` from the + // extern-C `outgoing` argument) is a valid CppWebSocket*; it + // outlives the handler — `handle_without_deinit` drops the + // ref before C++ can finalize. + ws: ws_ref + .outgoing_websocket + .map(|p| unsafe { CppWebSocketRef::new(p) }), + })); + // Backref so `handle_data` can drain the buffered slice ahead of + // fresh socket data, and so `deinit()` can detach from the box if + // teardown races ahead of the microtask drain. + ws_ref.initial_data_handler = NonNull::new(initial_data); + + // Use a higher-priority callback for the initial onData handler + // `queue_microtask_callback` takes an erased + // `(*mut c_void, unsafe extern "C" fn(*mut c_void))`; cast both. + global_this.queue_microtask_callback( + initial_data.cast::(), + InitialDataHandler::::handle, + ); } + // And lastly, ref the new websocket since C++ has a reference to it + ws_ref.ref_(); + + ws.cast::() + } + + pub extern "C" fn init( + outgoing: *mut CppWebSocket, + input_socket: *mut c_void, + global_this: &JSGlobalObject, + buffered_data: *mut u8, + buffered_data_len: usize, + deflate_params: Option<&websocket_deflate::Params>, + secure_ptr: *mut c_void, + ) -> *mut c_void { + let tcp = input_socket.cast::(); + let secure = if secure_ptr.is_null() { + None + } else { + Some(secure_ptr.cast::()) + }; + let ws = Self::new_ws(global_this, outgoing, secure, None, deflate_params); + // outlives this call. + let vm = global_this.bun_vm().as_mut(); + // `adopt_group` takes a closure to write the new socket. let group = { // reshaped for borrowck — `rare_data()` borrows `vm` @@ -1804,8 +1877,8 @@ impl WebSocket { }, ws, // SAFETY: `owner == ws` is a valid live allocation; raw-ptr field - // write avoids materializing a second `&mut` that would alias - // `ws_ref` above. + // write avoids materializing a `&mut WebSocket` around the + // callback. |owner, sock| unsafe { core::ptr::addr_of_mut!((*owner).tcp).write(sock) }, ) { // SAFETY: `ws` is the `heap::alloc` allocation just created @@ -1814,50 +1887,24 @@ impl WebSocket { return core::ptr::null_mut(); } - bun_core::handle_oom(ws_ref.send_buffer.ensure_total_capacity(2048)); - bun_core::handle_oom(ws_ref.receive_buffer.ensure_total_capacity(2048)); - ws_ref.poll_ref.r#ref(Self::vm_loop_ctx(global_this)); - - if buffered_data_len > 0 { + let buffered: Option> = if buffered_data_len > 0 { // SAFETY: buffered_data/len from C++; caller guarantees validity. // The upgrade client allocated this buffer via mimalloc // and transfers ownership to us. // The global allocator is also mimalloc, so `heap::take` // adopts the original allocation (no copy) and `Drop` will `mi_free` it. - let buffered_slice: Box<[u8]> = unsafe { + Some(unsafe { bun_core::heap::take(std::ptr::slice_from_raw_parts_mut( buffered_data, buffered_data_len, )) - }; - let initial_data = bun_core::heap::into_raw(Box::new(InitialDataHandler:: { - adopted: NonNull::new(ws), - slice: buffered_slice, - // We need to ref the outgoing websocket so that it doesn't get - // finalized before the initial data handler is called. - // SAFETY: outgoing is a valid CppWebSocket* (extern-C contract); - // it outlives the handler — `handle_without_deinit` drops the - // ref before C++ can finalize. - ws: NonNull::new(outgoing).map(|p| unsafe { CppWebSocketRef::new(p) }), - })); - // Backref so `handle_data` can drain the buffered slice ahead of - // fresh socket data, and so `deinit()` can detach from the box if - // teardown races ahead of the microtask drain. - ws_ref.initial_data_handler = NonNull::new(initial_data); - - // Use a higher-priority callback for the initial onData handler - // `queue_microtask_callback` takes an erased - // `(*mut c_void, unsafe extern "C" fn(*mut c_void))`; cast both. - global_this.queue_microtask_callback( - initial_data.cast::(), - InitialDataHandler::::handle, - ); - } - - // And lastly, ref the new websocket since C++ has a reference to it - ws_ref.ref_(); - - ws.cast::() + }) + } else { + None + }; + // SAFETY: `ws` is the live allocation created above with no other + // borrows. + unsafe { Self::finish_init(ws, global_this, buffered) } } /// Initialize a WebSocket client that uses a proxy tunnel for I/O. @@ -1882,92 +1929,34 @@ impl WebSocket { NonNull::new(p).expect("extern-C contract: tunnel_ptr is non-null") }; - // ref_count starts at 1: this is the I/O-layer ref, owned by the - // tunnel connection (analogous to the adopted-socket ref in init() - // that handle_close() releases). It is released in clear_data() when - // proxy_tunnel is detached. The ws.ref() below adds the C++ ref - // paired with m_connectedWebSocket. - // outlives this call. - let vm = global_this.bun_vm().as_mut(); - let ws = bun_core::heap::into_raw(Box::new(WebSocket:: { - ref_count: Cell::new(1), - tcp: Socket::::detached(), // No direct socket - using tunnel - outgoing_websocket: NonNull::new(outgoing), - receive_state: ReceiveState::NeedHeader, - receiving_type: Opcode::ResB, - receiving_is_final: true, - ping_frame_bytes: [0u8; 128 + 6], - ping_len: 0, - ping_received: false, - pong_received: false, - close_received: false, - close_frame_buffering: false, - receive_frame: 0, - receive_body_remain: 0, - receive_pending_chunk_len: 0, - receive_buffer: LinearFifo::>::init(), - send_buffer: LinearFifo::>::init(), - global_this: GlobalRef::from(global_this), - poll_ref: KeepAlive::init(), - header_fragment: None, - payload_length_frame_bytes: [0u8; 8], - payload_length_frame_len: 0, - initial_data_handler: None, - // reshaped for borrowck — `vm.event_loop()` returns a - // `&'static`-tied borrow that would lock `vm` for the rest of the - // fn; re-derive from `global_this` so `vm` stays usable below. - // SAFETY: bun_vm() never returns null; event_loop ptr is live for VM lifetime. - event_loop: global_this.bun_vm().event_loop_mut(), - deflate: None, - receiving_compressed: false, - message_is_compressed: false, - secure: None, - proxy_tunnel: Some(tunnel_owned), - })); - bun_core::scoped_log!(alloc, "new({}) = {:p}", Self::ALLOC_TYPE_NAME, ws); - // SAFETY: ws was just allocated via heap::alloc - let ws_ref = unsafe { &mut *ws }; - - if let Some(params) = deflate_params { - match WebSocketDeflate::init(*params, vm.rare_data()) { - Ok(deflate) => ws_ref.deflate = Some(deflate), - Err(_) => ws_ref.deflate = None, - } - } - - bun_core::handle_oom(ws_ref.send_buffer.ensure_total_capacity(2048)); - bun_core::handle_oom(ws_ref.receive_buffer.ensure_total_capacity(2048)); - ws_ref.poll_ref.r#ref(Self::vm_loop_ctx(global_this)); + // No direct socket — `tcp` stays detached and all I/O goes through + // the tunnel. + let ws = Self::new_ws( + global_this, + outgoing, + None, + Some(tunnel_owned), + deflate_params, + ); - if buffered_data_len > 0 { - // SAFETY: see `init()` — adopt the C++ mimalloc-owned buffer - // directly so it is freed (not leaked) when the handler drops. - let buffered_slice: Box<[u8]> = unsafe { + let buffered: Option> = if buffered_data_len > 0 { + // SAFETY: buffered_data/len from C++; caller guarantees validity. + // The upgrade client allocated this buffer via mimalloc + // and transfers ownership to us. + // The global allocator is also mimalloc, so `heap::take` + // adopts the original allocation (no copy) and `Drop` will `mi_free` it. + Some(unsafe { bun_core::heap::take(std::ptr::slice_from_raw_parts_mut( buffered_data, buffered_data_len, )) - }; - let initial_data = bun_core::heap::into_raw(Box::new(InitialDataHandler:: { - adopted: NonNull::new(ws), - slice: buffered_slice, - // SAFETY: outgoing is a valid CppWebSocket* (extern-C contract); - // it outlives the handler — `handle_without_deinit` drops the - // ref before C++ can finalize. - ws: NonNull::new(outgoing).map(|p| unsafe { CppWebSocketRef::new(p) }), - })); - ws_ref.initial_data_handler = NonNull::new(initial_data); - // `queue_microtask_callback` takes an erased - // `(*mut c_void, unsafe extern "C" fn(*mut c_void))`; cast both. - global_this.queue_microtask_callback( - initial_data.cast::(), - InitialDataHandler::::handle, - ); - } - - ws_ref.ref_(); - - ws.cast::() + }) + } else { + None + }; + // SAFETY: `ws` is the live allocation created above with no other + // borrows. + unsafe { Self::finish_init(ws, global_this, buffered) } } /// Handle data received from the proxy tunnel (already decrypted). diff --git a/src/http_types/h2.rs b/src/http_types/h2.rs index 47b3162f0845..2b7c50dd978e 100644 --- a/src/http_types/h2.rs +++ b/src/http_types/h2.rs @@ -336,3 +336,41 @@ impl Default for FullSettingsPayload { impl FullSettingsPayload { pub(crate) const BYTE_SIZE: usize = 42; } + +// ─── field validation (RFC 9113 §8.2.1) ───────── + +/// RFC 9110 §5.6.2 `tchar`, restricted to lowercase: RFC 9113 §8.2.1 requires +/// HTTP/2 field names to be lowercase, so uppercase tchars are rejected (or +/// normalized) by callers rather than accepted here. +#[inline] +pub const fn is_lower_tchar(c: u8) -> bool { + matches!( + c, + b'a'..=b'z' + | b'0'..=b'9' + | b'!' + | b'#' + | b'$' + | b'%' + | b'&' + | b'\'' + | b'*' + | b'+' + | b'-' + | b'.' + | b'^' + | b'_' + | b'`' + | b'|' + | b'~' + ) +} + +/// RFC 9113 §8.2.1: a field value MUST NOT contain NUL (0x00), LF (0x0a), or +/// CR (0x0d). HPACK is length-prefixed so these would otherwise pass through +/// verbatim, breaking the no-CR/LF invariant the HTTP/1.1 parser provides and +/// enabling header injection when values are forwarded downstream. +#[inline] +pub fn is_malformed_field_value(value: &[u8]) -> bool { + value.iter().any(|&c| matches!(c, 0 | b'\r' | b'\n')) +} diff --git a/src/ini/lib.rs b/src/ini/lib.rs index 5627368ebc2c..122334a76477 100644 --- a/src/ini/lib.rs +++ b/src/ini/lib.rs @@ -1497,7 +1497,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(_) => { @@ -1509,16 +1509,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(); @@ -1799,125 +1798,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 abea7ad087ce..aa8381e8bc53 100644 --- a/src/install/PackageInstall.rs +++ b/src/install/PackageInstall.rs @@ -1331,232 +1331,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<'_>, ) -> 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(bun_core::err!("NameTooLong")); - } - - 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, @@ -1564,10 +1485,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 { @@ -1799,37 +1716,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], ) -> 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)] { @@ -1864,99 +1762,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(bun_core::err!("NameTooLong")); - } - - 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[..], ); @@ -2558,3 +2408,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]) -> Result<(), bun_core::Error>, +) -> 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(bun_core::err!("NameTooLong")); + } + + 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 6533c9e3caa8..035e4970c415 100644 --- a/src/install/PackageInstaller.rs +++ b/src/install/PackageInstaller.rs @@ -367,6 +367,16 @@ 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 @@ -1806,74 +1816,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 { @@ -2117,73 +2073,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` @@ -2213,6 +2116,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 2159986104a5..8cc6c6e5ab72 100644 --- a/src/install/PackageManager.rs +++ b/src/install/PackageManager.rs @@ -1410,6 +1410,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 @@ -1907,129 +2030,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 @@ -2340,23 +2379,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 @@ -2365,114 +2391,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 913dd266d0fa..5a92a20aada0 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, +) -> Result<&'a mut TaskCallbackList, bun_core::Error> { + 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, +) -> Result<(), bun_core::Error> { + 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 { @@ -2187,6 +2158,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, @@ -2313,54 +2319,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)); } } } @@ -2474,46 +2453,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 e861062df2de..2f1e0855abc5 100644 --- a/src/install/PackageManager/WorkspacePackageJSONCache.rs +++ b/src/install/PackageManager/WorkspacePackageJSONCache.rs @@ -1,7 +1,7 @@ // maybe rename to `PackageJSONCache` if we cache more than workspaces use bun_collections::StringHashMap; -use bun_core::Error; +use bun_core::{Error, 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 @@ -224,6 +224,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 d1ab3976fe4f..0264d215802d 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 @@ -1488,6 +1427,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, +) -> Result<(), bun_core::Error> { + // 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( @@ -1525,63 +1507,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 93903e0e56ea..da9553917951 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 bump = bun_alloc::Arena::new(); - let json = match JSON::parse_package_json_utf8(&package_json_source, log, &bump) { - Ok(j) => j, - 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 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<(), bun_core::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<(), bun_core::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 bump = bun_alloc::Arena::new(); - let json = match JSON::parse_package_json_utf8(&package_json_source, log, &bump) { - Ok(j) => j, - 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 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], +) -> Result<(Vec, Package), bun_core::Error> { + 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 bump = bun_alloc::Arena::new(); + let json = match JSON::parse_package_json_utf8(&package_json_source, log, &bump) { + Ok(j) => j, + 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 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 cc6b53c3a3e4..c3635407b143 100644 --- a/src/install/PackageManager/runTasks.rs +++ b/src/install/PackageManager/runTasks.rs @@ -446,16 +446,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; @@ -498,16 +489,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; } @@ -702,45 +684,11 @@ pub fn run_tasks( .fail .unwrap_or_else(|| bun_core::err!("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!( @@ -767,16 +715,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); @@ -788,50 +727,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 => bun_core::err!("TarballHTTP400"), - 401 => bun_core::err!("TarballHTTP401"), - 402 => bun_core::err!("TarballHTTP402"), - 403 => bun_core::err!("TarballHTTP403"), - 404 => bun_core::err!("TarballHTTP404"), - 405..=499 => bun_core::err!("TarballHTTP4xx"), - _ => bun_core::err!("TarballHTTP5xx"), - }; + let err = match response.status_code { + 400 => bun_core::err!("TarballHTTP400"), + 401 => bun_core::err!("TarballHTTP401"), + 402 => bun_core::err!("TarballHTTP402"), + 403 => bun_core::err!("TarballHTTP403"), + 404 => bun_core::err!("TarballHTTP404"), + 405..=499 => bun_core::err!("TarballHTTP4xx"), + _ => bun_core::err!("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!( @@ -852,16 +762,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); @@ -1562,6 +1463,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: bun_core::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 bf38804aeb70..c47f63a8a11d 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::ShellCompletions; use crate::bun_fs::FileSystem; use crate::bun_json as json; @@ -617,25 +616,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 81c88061827f..d6fdb2a7839e 100644 --- a/src/install/hosted_git_info.rs +++ b/src/install/hosted_git_info.rs @@ -1074,6 +1074,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::*; @@ -1092,21 +1131,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( @@ -1115,20 +1149,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) } } @@ -1150,21 +1180,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( @@ -1173,20 +1198,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) } } @@ -1210,26 +1231,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( @@ -1239,20 +1250,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( @@ -1263,22 +1270,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) } } @@ -1300,22 +1301,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( @@ -1324,20 +1319,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) } } @@ -1347,6 +1338,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"/"); @@ -1391,54 +1418,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(); @@ -1449,28 +1476,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> { @@ -1528,176 +1544,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, + ) } } @@ -1725,26 +1580,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( @@ -1754,20 +1599,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 40d22bd7e0b6..25b5c029ebf8 100644 --- a/src/install/isolated_install.rs +++ b/src/install/isolated_install.rs @@ -2407,6 +2407,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( @@ -2424,24 +2447,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; } } @@ -2473,24 +2479,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; } } @@ -2517,24 +2506,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 3c6052722054..bd03ad6ad8b4 100644 --- a/src/install/npm.rs +++ b/src/install/npm.rs @@ -643,6 +643,39 @@ pub(crate) fn negatable_from_json(expr: &JSON::Expr) -> Result Ok(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: &JSON::Expr, + bump: &bun_alloc::Arena, + 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_expr) = version + .get(b"bundleDependencies") + .or_else(|| version.get(b"bundledDependencies")) + else { + return Ok(()); + }; + match &bundled_deps_expr.data { + JSON::ExprData::EBoolean(boolean) => { + *bundle_all_deps = boolean.value; + } + JSON::ExprData::EArray(arr) => { + for bundled_dep in arr.slice() { + let Some(s) = bundled_dep.as_string(bump) else { + continue; + }; + bundled_deps_set.insert(s)?; + } + } + _ => {} + } + Ok(()) +} + // ────────────────────────────────────────────────────────────────────────── #[repr(C)] @@ -1996,7 +2029,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; @@ -2165,35 +2198,12 @@ impl PackageManifest { } } - bundled_deps_set.map.clear_retaining_capacity(); - bundle_all_deps = false; - if let Some(bundled_deps_expr) = prop - .value - .as_ref() - .unwrap() - .get(b"bundleDependencies") - .or_else(|| { - prop.value - .as_ref() - .expect("infallible: prop has value") - .get(b"bundledDependencies") - }) - { - match &bundled_deps_expr.data { - JSON::ExprData::EBoolean(boolean) => { - bundle_all_deps = boolean.value; - } - JSON::ExprData::EArray(arr) => { - for bundled_dep in arr.slice() { - let Some(s) = bundled_dep.as_string(&bump) else { - continue; - }; - bundled_deps_set.insert(s)?; - } - } - _ => {} - } - } + extract_bundled_deps( + prop.value.as_ref().expect("infallible: prop has value"), + &bump, + &mut bundled_deps_set, + &mut bundle_all_deps, + )?; for pair in &DEPENDENCY_GROUPS { if let Some(versioned_deps) = prop @@ -2432,35 +2442,12 @@ impl PackageManifest { continue; } - bundled_deps_set.map.clear_retaining_capacity(); - bundle_all_deps = false; - if let Some(bundled_deps_expr) = prop - .value - .as_ref() - .unwrap() - .get(b"bundleDependencies") - .or_else(|| { - prop.value - .as_ref() - .expect("infallible: prop has value") - .get(b"bundledDependencies") - }) - { - match &bundled_deps_expr.data { - JSON::ExprData::EBoolean(boolean) => { - bundle_all_deps = boolean.value; - } - JSON::ExprData::EArray(arr) => { - for bundled_dep in arr.slice() { - let Some(s) = bundled_dep.as_string(&bump) else { - continue; - }; - bundled_deps_set.insert(s)?; - } - } - _ => {} - } - } + extract_bundled_deps( + prop.value.as_ref().expect("infallible: prop has value"), + &bump, + &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 31ee66421c59..51c704b0c519 100644 --- a/src/install/yarn.rs +++ b/src/install/yarn.rs @@ -1808,169 +1808,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()), @@ -1993,7 +1848,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 2e53891c22fa..53a3cca14801 100644 --- a/src/io/PipeWriter.rs +++ b/src/io/PipeWriter.rs @@ -2363,44 +2363,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(); @@ -2425,42 +2437,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 942da6b2d8d8..46fbeac0d281 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))] @@ -287,6 +286,107 @@ 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) + } + + 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 @@ -370,33 +470,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. @@ -437,13 +511,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) - } - 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); @@ -459,19 +526,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) { @@ -586,11 +640,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"); @@ -614,11 +663,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_, @@ -1349,25 +1393,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(), } @@ -1375,7 +1443,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) } @@ -1384,13 +1452,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); } } @@ -1399,29 +1468,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); } } @@ -1431,7 +1500,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; @@ -1441,16 +1510,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 572945b71d8a..528d53f4dd93 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/js/internal/http.ts b/src/js/internal/http.ts index ecd92d4c4f01..c464498e424c 100644 --- a/src/js/internal/http.ts +++ b/src/js/internal/http.ts @@ -478,6 +478,88 @@ function filterEnvForProxies(env) { }; } +// Stub members shared by `FakeSocket` (internal/http/FakeSocket.ts) and +// `NodeHTTPServerSocket` (node/_http_server.ts). They are copied onto each +// class's prototype (instead of using a base class) so the prototype chain +// stays `Socket.prototype -> Duplex.prototype`, matching `net.Socket`. +const { constructor: _socketStubConstructor, ...socketStubDescriptors } = Object.getOwnPropertyDescriptors( + class { + declare connecting: boolean; + declare readable: boolean; + declare writable: boolean; + declare writableLength: number; + declare address: () => any; + + connect(_port, _host, _connectListener) { + return this; + } + + get bufferSize() { + return this.writableLength; + } + + get pending() { + return this.connecting; + } + + get readyState() { + if (this.connecting) return "opening"; + if (this.readable) { + return this.writable ? "open" : "readOnly"; + } else { + return this.writable ? "writeOnly" : "closed"; + } + } + + ref() { + return this; + } + + get remoteAddress() { + return this.address()?.address; + } + + set remoteAddress(val) { + // initialize the object so that other properties wouldn't be lost + this.address().address = val; + } + + get remotePort() { + return this.address()?.port; + } + + set remotePort(val) { + // initialize the object so that other properties wouldn't be lost + this.address().port = val; + } + + get remoteFamily() { + return this.address()?.family; + } + + set remoteFamily(val) { + // initialize the object so that other properties wouldn't be lost + this.address().family = val; + } + + resetAndDestroy() {} + + setKeepAlive(_enable = false, _initialDelay = 0) {} + + setNoDelay(_noDelay = true) { + return this; + } + + unref() { + return this; + } + }.prototype, +); + +function installSocketStubs(SocketClass: { prototype: object }) { + Object.defineProperties(SocketClass.prototype, socketStubDescriptors); +} + export { Headers, METHODS, @@ -507,6 +589,7 @@ export { headerStateSymbol, headersSymbol, headersTuple, + installSocketStubs, isAbortError, isTlsSymbol, kAbortController, diff --git a/src/js/internal/http/FakeSocket.ts b/src/js/internal/http/FakeSocket.ts index 3b7c9a08b767..1b7864f1468e 100644 --- a/src/js/internal/http/FakeSocket.ts +++ b/src/js/internal/http/FakeSocket.ts @@ -1,4 +1,4 @@ -const { kInternalSocketData, serverSymbol } = require("internal/http"); +const { kInternalSocketData, serverSymbol, installSocketStubs } = require("internal/http"); const { kAutoDestroyed } = require("internal/shared"); const { Duplex } = require("internal/stream"); @@ -24,13 +24,6 @@ var FakeSocket = class Socket extends Duplex { (internalData = this[kInternalSocketData])?.[0]?.[serverSymbol]?.requestIP(internalData[2]) ?? {}); } - get bufferSize() { - return this.writableLength; - } - - connect(_port, _host, _connectListener) { - return this; - } _onTimeout = function () { this.emit("timeout"); }; @@ -55,60 +48,8 @@ var FakeSocket = class Socket extends Duplex { return 80; } - get pending() { - return this.connecting; - } - _read(_size) {} - get readyState() { - if (this.connecting) return "opening"; - if (this.readable) { - return this.writable ? "open" : "readOnly"; - } else { - return this.writable ? "writeOnly" : "closed"; - } - } - - ref() { - return this; - } - - get remoteAddress() { - return this.address()?.address; - } - - set remoteAddress(val) { - // initialize the object so that other properties wouldn't be lost - this.address().address = val; - } - - get remotePort() { - return this.address()?.port; - } - - set remotePort(val) { - // initialize the object so that other properties wouldn't be lost - this.address().port = val; - } - - get remoteFamily() { - return this.address()?.family; - } - - set remoteFamily(val) { - // initialize the object so that other properties wouldn't be lost - this.address().family = val; - } - - resetAndDestroy() {} - - setKeepAlive(_enable = false, _initialDelay = 0) {} - - setNoDelay(_noDelay = true) { - return this; - } - setTimeout(timeout, callback) { const socketData = this[kInternalSocketData]; if (!socketData) return; // sometimes 'this' is Socket not FakeSocket @@ -118,10 +59,6 @@ var FakeSocket = class Socket extends Duplex { return this; } - unref() { - return this; - } - _write(_chunk, _encoding, _callback) {} destroy() { @@ -130,6 +67,7 @@ var FakeSocket = class Socket extends Duplex { } }; +installSocketStubs(FakeSocket); Object.defineProperty(FakeSocket, "name", { value: "Socket" }); export default { diff --git a/src/js/internal/sql/mysql.ts b/src/js/internal/sql/mysql.ts index f94578e56edb..02cea7b71f9a 100644 --- a/src/js/internal/sql/mysql.ts +++ b/src/js/internal/sql/mysql.ts @@ -1,11 +1,16 @@ import type { MySQLErrorOptions } from "internal/sql/errors"; import type { Query } from "./query"; -import type { ArrayType, DatabaseAdapter, SQLArrayParameter, SQLHelper, SQLResultArray, SSLMode } from "./shared"; -const { SQLHelper, SSLMode, SQLResultArray, buildDefinedColumnsAndQuery } = require("internal/sql/shared"); +import type { ArrayType, DatabaseAdapter, SQLArrayParameter, SQLCommand, SQLResultArray, SSLMode } from "./shared"; +const { + SQLResultArray, + BasePooledConnection, + BaseSQLAdapter, + createPooledConnectionHandle, + getHelperCommandFromDetect, +} = require("internal/sql/shared"); const { - Query, SQLQueryFlags, - symbols: { _strings, _values, _results, _handle }, + symbols: { _results, _handle }, } = require("internal/sql/query"); const { MySQLError } = require("internal/sql/errors"); @@ -114,354 +119,52 @@ export interface MySQLDotZig { ) => $ZigGeneratedClasses.MySQLQuery; } -const enum SQLCommand { - insert = 0, - update = 1, - updateSet = 2, - where = 3, - in = 4, - none = -1, -} -export type { SQLCommand }; - -function commandToString(command: SQLCommand): string { - switch (command) { - case SQLCommand.insert: - return "INSERT"; - case SQLCommand.updateSet: - case SQLCommand.update: - return "UPDATE"; - case SQLCommand.in: - case SQLCommand.where: - return "WHERE"; - default: - return ""; - } -} - -function detectCommand(query: string): SQLCommand { - const text = query.toLowerCase().trim(); - const text_len = text.length; - - let token = ""; - let command = SQLCommand.none; - let quoted = false; - // we need to reverse search so we find the closest command to the parameter - for (let i = text_len - 1; i >= 0; i--) { - const char = text[i]; - switch (char) { - case " ": // Space - case "\n": // Line feed - case "\t": // Tab character - case "\r": // Carriage return - case "\f": // Form feed - case "\v": { - switch (token) { - case "insert": { - return SQLCommand.insert; - } - case "update": { - return SQLCommand.update; - } - case "where": { - return SQLCommand.where; - } - case "set": { - return SQLCommand.updateSet; - } - case "in": { - return SQLCommand.in; - } - default: { - token = ""; - continue; - } - } - } - default: { - // skip quoted commands - if (char === '"') { - quoted = !quoted; - continue; - } - if (!quoted) { - token = char + token; - } - } - } - } - if (token) { - switch (token) { - case "insert": - return SQLCommand.insert; - case "update": - return SQLCommand.update; - case "where": - return SQLCommand.where; - case "set": - return SQLCommand.updateSet; - case "in": - case "any": - case "all": - return SQLCommand.in; - default: - return SQLCommand.none; - } - } - return command; -} -const enum PooledConnectionState { - pending = 0, - connected = 1, - closed = 2, -} - -const enum PooledConnectionFlags { - /// canBeConnected is used to indicate that at least one time we were able to connect to the database - canBeConnected = 1 << 0, - /// reserved is used to indicate that the connection is currently reserved - reserved = 1 << 1, - /// preReserved is used to indicate that the connection will be reserved in the future when queryCount drops to 0 - preReserved = 1 << 2, -} - -function onQueryFinish(this: PooledMySQLConnection, onClose: (err: Error) => void) { - this.queries.delete(onClose); - this.adapter.release(this); -} - -function closeNT(onClose: (err: Error) => void, err: Error | null) { - onClose(err as Error); -} -class PooledMySQLConnection { - private static async createConnection( - options: Bun.SQL.__internal.DefinedPostgresOrMySQLOptions, - onConnected: (err: Error | null, connection: $ZigGeneratedClasses.MySQLConnection) => void, - onClose: (err: Error | null) => void, - ): Promise<$ZigGeneratedClasses.MySQLConnection | null> { - const { - hostname, - port, - username, - tls, - query, - database, - sslMode, - idleTimeout = 0, - connectionTimeout = 30 * 1000, - maxLifetime = 0, - prepare = true, - path, - allowPublicKeyRetrieval = false, - } = options; - - let password: Bun.MaybePromise | string | undefined | (() => Bun.MaybePromise) = options.password; - - try { - if (typeof password === "function") { - password = password(); - } - - if (password && $isPromise(password)) { - password = await password; - } - - return createMySQLConnection( - hostname, - Number(port), - username || "", - password || "", - database || "", - // > The default value for sslmode is prefer. As is shown in the table, this - // makes no sense from a security point of view, and it only promises - // performance overhead if possible. It is only provided as the default for - // backward compatibility, and is not recommended in secure deployments. - sslMode || SSLMode.disable, - tls || null, - query || "", - path || "", - onConnected, - onClose, - idleTimeout, - connectionTimeout, - maxLifetime, - !prepare, - !!allowPublicKeyRetrieval, - ); - } catch (e) { - process.nextTick(closeNT, onClose, e); - return null; - } - } - - adapter: MySQLAdapter; - connection: $ZigGeneratedClasses.MySQLConnection | null = null; - state: PooledConnectionState = PooledConnectionState.pending; - storedError: Error | null = null; - queries: Set<(err: Error) => void> = new Set(); - onFinish: ((err: Error | null) => void) | null = null; - connectionInfo: Bun.SQL.__internal.DefinedPostgresOrMySQLOptions; - flags: number = 0; - /// queryCount is used to indicate the number of queries using the connection, if a connection is reserved or if its a transaction queryCount will be 1 independently of the number of queries - queryCount: number = 0; - - #onConnected(err, connection) { - if (err) { - err = wrapError(err); - } else { - this.connection = connection; - } - - const connectionInfo = this.connectionInfo; - if (connectionInfo?.onconnect) { - connectionInfo.onconnect(err); - } - this.storedError = err; +class PooledMySQLConnection extends BasePooledConnection<$ZigGeneratedClasses.MySQLConnection> { + protected handleConnected(err: any, connection?: $ZigGeneratedClasses.MySQLConnection) { if (!err) { - this.flags |= PooledConnectionFlags.canBeConnected; - } - this.state = err ? PooledConnectionState.closed : PooledConnectionState.connected; - const onFinish = this.onFinish; - if (onFinish) { - this.queryCount = 0; - this.flags &= ~PooledConnectionFlags.reserved; - this.flags &= ~PooledConnectionFlags.preReserved; - - // pool is closed, lets finish the connection - // pool is closed, lets finish the connection - if (err) { - onFinish(err); - } else { - this.connection?.close(); - } - return; + this.connection = connection!; } - this.adapter.release(this, true); + super.handleConnected(err); } - #onClose(err) { - if (err) { - err = wrapError(err); - } - const connectionInfo = this.connectionInfo; - if (connectionInfo?.onclose) { - connectionInfo.onclose(err); - } - this.state = PooledConnectionState.closed; - this.connection = null; - this.storedError = err; - - // remove from ready connections if its there - this.adapter.readyConnections.delete(this); - const queries = new Set(this.queries); - this.queries?.clear?.(); - this.queryCount = 0; - this.flags &= ~PooledConnectionFlags.reserved; - - // notify all queries that the connection is closed - for (const onClose of queries) { - onClose(err); - } - const onFinish = this.onFinish; - if (onFinish) { - onFinish(err); - } - - this.adapter.release(this, true); - } - - constructor(connectionInfo: Bun.SQL.__internal.DefinedMySQLOptions, adapter: MySQLAdapter) { - this.state = PooledConnectionState.pending; - this.adapter = adapter; - this.connectionInfo = connectionInfo; - this.#startConnection(); - } - - #startConnection() { - PooledMySQLConnection.createConnection(this.connectionInfo, this.#onConnected.bind(this), this.#onClose.bind(this)); - } - - onClose(onClose: (err: Error) => void) { - this.queries.add(onClose); - } - - bindQuery(query: Query, onClose: (err: Error) => void) { - this.queries.add(onClose); - query.finally(onQueryFinish.bind(this, onClose)); + protected startConnection() { + createPooledConnectionHandle( + createMySQLConnection, + this.connectionInfo, + this.handleConnected.bind(this), + this.handleClose.bind(this), + true, + ); } - #doRetry() { - if (this.adapter.closed) { - return; - } - // reset error and state - this.storedError = null; - this.state = PooledConnectionState.pending; - // retry connection - this.#startConnection(); + protected wrapError(error: any): Error { + return wrapError(error); } - close() { - try { - if (this.state === PooledConnectionState.connected) { - this.connection?.close(); - } - } catch {} - } - flush() { - this.connection?.flush(); - } - retry() { - // if pool is closed, we can't retry - if (this.adapter.closed) { - return false; - } - // we need to reconnect - // lets use a retry strategy - // we can only retry if one day we are able to connect - if (this.flags & PooledConnectionFlags.canBeConnected) { - this.#doRetry(); - } else { - // analyse type of error to see if we can retry - switch (this.storedError?.code) { - case "ERR_MYSQL_PASSWORD_REQUIRED": - case "ERR_MYSQL_MISSING_AUTH_DATA": - case "ERR_MYSQL_FAILED_TO_ENCRYPT_PASSWORD": - case "ERR_MYSQL_INVALID_PUBLIC_KEY": - case "ERR_MYSQL_UNSUPPORTED_PROTOCOL_VERSION": - case "ERR_MYSQL_UNSUPPORTED_AUTH_PLUGIN": - case "ERR_MYSQL_AUTHENTICATION_FAILED": - // we can't retry these are authentication errors - return false; - default: - // we can retry - this.#doRetry(); - } + protected isNonRetryableError(code: string | undefined): boolean { + switch (code) { + case "ERR_MYSQL_PASSWORD_REQUIRED": + case "ERR_MYSQL_MISSING_AUTH_DATA": + case "ERR_MYSQL_FAILED_TO_ENCRYPT_PASSWORD": + case "ERR_MYSQL_INVALID_PUBLIC_KEY": + case "ERR_MYSQL_UNSUPPORTED_PROTOCOL_VERSION": + case "ERR_MYSQL_UNSUPPORTED_AUTH_PLUGIN": + case "ERR_MYSQL_AUTHENTICATION_FAILED": + // we can't retry these are authentication errors + return true; + default: + return false; } - return true; } } class MySQLAdapter + extends BaseSQLAdapter implements DatabaseAdapter { - public readonly connectionInfo: Bun.SQL.__internal.DefinedPostgresOrMySQLOptions; - - public readonly connections: PooledMySQLConnection[]; - public readonly readyConnections: Set = new Set(); - - public waitingQueue: Array<(err: Error | null, result: any) => void> = []; - public reservedQueue: Array<(err: Error | null, result: any) => void> = []; - - public poolStarted: boolean = false; - public closed: boolean = false; - public totalQueries: number = 0; - public onAllQueriesFinished: (() => void) | null = null; - - constructor(connectionInfo: Bun.SQL.__internal.DefinedPostgresOrMySQLOptions) { - this.connectionInfo = connectionInfo; - this.connections = new Array(connectionInfo.max); + protected createPooledConnection(): PooledMySQLConnection { + return new PooledMySQLConnection(this.connectionInfo, this); } escapeIdentifier(str: string) { @@ -488,25 +191,12 @@ class MySQLAdapter code: "ERR_MYSQL_INVALID_TRANSACTION_STATE", }); } - supportsReservedConnections() { - return true; - } - - getConnectionForQuery(pooledConnection: PooledMySQLConnection) { - return pooledConnection.connection; - } - - attachConnectionCloseHandler(connection: PooledMySQLConnection, handler: () => void): void { - if (connection.onClose) { - connection.onClose(handler); - } + unsafeTransactionError() { + return new MySQLError("Only use sql.begin, sql.reserved or max: 1", { + code: "ERR_MYSQL_UNSAFE_TRANSACTION", + }); } - detachConnectionCloseHandler(connection: PooledMySQLConnection, handler: () => void): void { - if (connection.queries) { - connection.queries.delete(handler); - } - } array(_values: any[], _typeNameOrID?: number | ArrayType): SQLArrayParameter { throw new Error("MySQL doesn't support arrays"); } @@ -542,28 +232,6 @@ class MySQLAdapter }; } - validateTransactionOptions(options: string): { valid: boolean; error?: string } { - // The string is interpolated into `START TRANSACTION ${options}`, so refuse anything - // that could terminate the statement or start a new one. - if (!/^[A-Za-z ,]*$/.test(options)) { - return { - valid: false, - error: "Transaction options can only contain letters, spaces, and commas.", - }; - } - return { valid: true }; - } - - validateDistributedTransactionName(name: string): { valid: boolean; error?: string } { - if (name.indexOf("'") !== -1) { - return { - valid: false, - error: "Distributed transaction name cannot contain single quotes.", - }; - } - return { valid: true }; - } - getCommitDistributedSQL(name: string): string { const validation = this.validateDistributedTransactionName(name); if (!validation.valid) { @@ -581,16 +249,7 @@ class MySQLAdapter } createQueryHandle(sql: string, values: unknown[], flags: number) { - if (!(flags & SQLQueryFlags.allowUnsafeTransaction)) { - if (this.connectionInfo.max !== 1) { - const upperCaseSqlString = sql.toUpperCase().trim(); - if (upperCaseSqlString.startsWith("BEGIN") || upperCaseSqlString.startsWith("START TRANSACTION")) { - throw new MySQLError("Only use sql.begin, sql.reserved or max: 1", { - code: "ERR_MYSQL_UNSAFE_TRANSACTION", - }); - } - } - } + this.checkUnsafeTransaction(sql, flags); return createMySQLQuery( sql, @@ -602,560 +261,15 @@ class MySQLAdapter ); } - maxDistribution() { - if (!this.waitingQueue.length) return 0; - const result = Math.ceil((this.waitingQueue.length + this.totalQueries) / this.connections.length); - return result ? result : 1; - } - - flushConcurrentQueries() { - const maxDistribution = this.maxDistribution(); - if (maxDistribution === 0) { - return; - } - - while (true) { - const nonReservedConnections = Array.from(this.readyConnections).filter( - c => !(c.flags & PooledConnectionFlags.preReserved) && c.queryCount < maxDistribution, - ); - if (nonReservedConnections.length === 0) { - return; - } - const orderedConnections = nonReservedConnections.sort((a, b) => a.queryCount - b.queryCount); - for (const connection of orderedConnections) { - const pending = this.waitingQueue.shift(); - if (!pending) { - return; - } - connection.queryCount++; - this.totalQueries++; - pending(null, connection); - } - } - } - - release(connection: PooledMySQLConnection, connectingEvent: boolean = false) { - if (!connectingEvent) { - connection.queryCount--; - this.totalQueries--; - } - const currentQueryCount = connection.queryCount; - if (currentQueryCount == 0) { - connection.flags &= ~PooledConnectionFlags.reserved; - connection.flags &= ~PooledConnectionFlags.preReserved; - } - if (this.onAllQueriesFinished) { - // we are waiting for all queries to finish, lets check if we can call it - if (!this.hasPendingQueries()) { - this.onAllQueriesFinished(); - } - } - - if (connection.state !== PooledConnectionState.connected) { - // connection is not ready - if (connection.storedError) { - // this connection got a error but maybe we can wait for another - - if (this.hasConnectionsAvailable()) { - return; - } - - const waitingQueue = this.waitingQueue; - const reservedQueue = this.reservedQueue; - - this.waitingQueue = []; - this.reservedQueue = []; - // we have no connections available so lets fails - for (const pending of waitingQueue) { - pending(connection.storedError, connection); - } - for (const pending of reservedQueue) { - pending(connection.storedError, connection); - } - } - return; - } - - if (currentQueryCount == 0) { - // ok we can actually bind reserved queries to it - const pendingReserved = this.reservedQueue.shift(); - if (pendingReserved) { - connection.flags |= PooledConnectionFlags.reserved; - connection.queryCount++; - this.totalQueries++; - // we have a connection waiting for a reserved connection lets prioritize it - pendingReserved(connection.storedError, connection); - return; - } - } - this.readyConnections.add(connection); - this.flushConcurrentQueries(); - } - - hasConnectionsAvailable() { - if (this.readyConnections.size > 0) return true; - if (this.poolStarted) { - const pollSize = this.connections.length; - for (let i = 0; i < pollSize; i++) { - const connection = this.connections[i]; - if (connection.state !== PooledConnectionState.closed) { - // some connection is connecting or connected - return true; - } - } - } - return false; - } - - hasPendingQueries() { - if (this.waitingQueue.length > 0 || this.reservedQueue.length > 0) return true; - if (this.poolStarted) { - return this.totalQueries > 0; - } - return false; + getHelperCommand(query: string): SQLCommand { + return getHelperCommandFromDetect(query, true); } - isConnected() { - if (this.readyConnections.size > 0) { - return true; - } - if (this.poolStarted) { - const pollSize = this.connections.length; - for (let i = 0; i < pollSize; i++) { - const connection = this.connections[i]; - if (connection.state === PooledConnectionState.connected) { - return true; - } - } - } - return false; - } - flush() { - if (this.closed) { - return; - } - if (this.poolStarted) { - const pollSize = this.connections.length; - for (let i = 0; i < pollSize; i++) { - const connection = this.connections[i]; - if (connection.state === PooledConnectionState.connected) { - connection.connection?.flush(); - } - } - } - } - - async #close() { - let pending; - while ((pending = this.waitingQueue.shift())) { - pending(this.connectionClosedError(), null); - } - while (this.reservedQueue.length > 0) { - const pendingReserved = this.reservedQueue.shift(); - if (pendingReserved) { - pendingReserved(this.connectionClosedError(), null); - } - } - - const promises: Array> = []; - - if (this.poolStarted) { - this.poolStarted = false; - const pollSize = this.connections.length; - for (let i = 0; i < pollSize; i++) { - const connection = this.connections[i]; - switch (connection.state) { - case PooledConnectionState.pending: - { - const { promise, resolve } = Promise.withResolvers(); - connection.onFinish = resolve; - promises.push(promise); - connection.connection?.close(); - } - break; - - case PooledConnectionState.connected: - { - const { promise, resolve } = Promise.withResolvers(); - connection.onFinish = resolve; - promises.push(promise); - connection.connection?.close(); - } - break; - } - // clean connection reference - // @ts-ignore - this.connections[i] = null; - } - } - - this.readyConnections.clear(); - this.waitingQueue.length = 0; - return Promise.all(promises); - } - - async close(options?: { timeout?: number }) { - if (this.closed) { - return; - } - - let timeout = options?.timeout; - if (timeout) { - timeout = Number(timeout); - if (timeout > 2 ** 31 || timeout < 0 || timeout !== timeout) { - throw $ERR_INVALID_ARG_VALUE("options.timeout", timeout, "must be a non-negative integer less than 2^31"); - } - - this.closed = true; - if (timeout === 0 || !this.hasPendingQueries()) { - // close immediately - await this.#close(); - return; - } - - const { promise, resolve } = Promise.withResolvers(); - const timer = setTimeout(() => { - // timeout is reached, lets close and probably fail some queries - this.#close().finally(resolve); - }, timeout * 1000); - timer.unref(); // dont block the event loop - - this.onAllQueriesFinished = () => { - clearTimeout(timer); - // everything is closed, lets close the pool - this.#close().finally(resolve); - }; - - return promise; - } else { - this.closed = true; - if (!this.hasPendingQueries()) { - // close immediately - await this.#close(); - return; - } - - // gracefully close the pool - const { promise, resolve } = Promise.withResolvers(); - - this.onAllQueriesFinished = () => { - // everything is closed, lets close the pool - this.#close().finally(resolve); - }; - - return promise; - } - } - - /** - * @param {function} onConnected - The callback function to be called when the connection is established. - * @param {boolean} reserved - Whether the connection is reserved, if is reserved the connection will not be released until release is called, if not release will only decrement the queryCount counter - */ - connect(onConnected: (err: Error | null, result: any) => void, reserved: boolean = false) { - if (this.closed) { - return onConnected(this.connectionClosedError(), null); - } - - if (this.readyConnections.size === 0) { - // no connection ready lets make some - let retry_in_progress = false; - let all_closed = true; - let storedError: Error | null = null; - - if (this.poolStarted) { - // we already started the pool - // lets check if some connection is available to retry - const pollSize = this.connections.length; - for (let i = 0; i < pollSize; i++) { - const connection = this.connections[i]; - // we need a new connection and we have some connections that can retry - if (connection.state === PooledConnectionState.closed) { - if (connection.retry()) { - // lets wait for connection to be released - if (!retry_in_progress) { - // avoid adding to the queue twice, we wanna to retry every available pool connection - retry_in_progress = true; - if (reserved) { - // we are not sure what connection will be available so we dont pre reserve - this.reservedQueue.push(onConnected); - } else { - this.waitingQueue.push(onConnected); - } - } - } else { - // we have some error, lets grab it and fail if unable to start a connection - storedError = connection.storedError; - } - } else { - // we have some pending or open connections - all_closed = false; - } - } - if (!all_closed && !retry_in_progress) { - // is possible to connect because we have some working connections, or we are just without network for some reason - // wait for connection to be released or fail - if (reserved) { - // we are not sure what connection will be available so we dont pre reserve - this.reservedQueue.push(onConnected); - } else { - this.waitingQueue.push(onConnected); - } - } else if (!retry_in_progress) { - // impossible to connect or retry - onConnected(storedError ?? this.connectionClosedError(), null); - } - return; - } - // we never started the pool, lets start it - if (reserved) { - this.reservedQueue.push(onConnected); - } else { - this.waitingQueue.push(onConnected); - } - this.poolStarted = true; - const pollSize = this.connections.length; - // pool is always at least 1 connection - const firstConnection = new PooledMySQLConnection(this.connectionInfo, this); - this.connections[0] = firstConnection; - if (reserved) { - firstConnection.flags |= PooledConnectionFlags.preReserved; // lets pre reserve the first connection - } - for (let i = 1; i < pollSize; i++) { - this.connections[i] = new PooledMySQLConnection(this.connectionInfo, this); - } - return; - } - if (reserved) { - let connectionWithLeastQueries: PooledMySQLConnection | null = null; - let leastQueries = Infinity; - for (const connection of this.readyConnections) { - if (connection.flags & PooledConnectionFlags.preReserved || connection.flags & PooledConnectionFlags.reserved) - continue; - const queryCount = connection.queryCount; - if (queryCount > 0) { - if (queryCount < leastQueries) { - leastQueries = queryCount; - connectionWithLeastQueries = connection; - } - continue; - } - connection.flags |= PooledConnectionFlags.reserved; - connection.queryCount++; - this.totalQueries++; - this.readyConnections.delete(connection); - onConnected(null, connection); - return; - } - - if (connectionWithLeastQueries) { - // lets mark the connection with the least queries as preReserved if any - connectionWithLeastQueries.flags |= PooledConnectionFlags.preReserved; - } - - // no connection available to be reserved lets wait for a connection to be released - this.reservedQueue.push(onConnected); - } else { - this.waitingQueue.push(onConnected); - this.flushConcurrentQueries(); - } - } - - normalizeQuery(strings: string | TemplateStringsArray, values: unknown[], binding_idx = 1): [string, unknown[]] { - if (typeof strings === "string") { - // identifier or unsafe query - return [strings, values || []]; - } - - if (!$isArray(strings)) { - // we should not hit this path - throw new SyntaxError("Invalid query: SQL Fragment cannot be executed or was misused"); - } - - const str_len = strings.length; - if (str_len === 0) { - return ["", []]; - } - - let binding_values: any[] = []; - let query = ""; - - for (let i = 0; i < str_len; i++) { - const string = strings[i]; - - if (typeof string === "string") { - query += string; - - if (values.length > i) { - const value = values[i]; - - if (value instanceof Query) { - const q = value as Query; - const [sub_query, sub_values] = this.normalizeQuery(q[_strings], q[_values], binding_idx); - - query += sub_query; - for (let j = 0; j < sub_values.length; j++) { - binding_values.push(sub_values[j]); - } - binding_idx += sub_values.length; - } else if (value instanceof SQLHelper) { - const command = detectCommand(query); - // only selectIn, insert, update, updateSet are allowed - if (command === SQLCommand.none || command === SQLCommand.where) { - throw new SyntaxError("Helpers are only allowed for INSERT, UPDATE and IN commands"); - } - const { columns, value: items } = value as SQLHelper; - const columnCount = columns.length; - if (columnCount === 0 && command !== SQLCommand.in) { - throw new SyntaxError(`Cannot ${commandToString(command)} with no columns`); - } - const lastColumnIndex = columns.length - 1; - - if (command === SQLCommand.insert) { - // - // insert into users ${sql(users)} or insert into users ${sql(user)} - // - - // Build column list while determining which columns have at least one defined value - const { definedColumns, columnsSql } = buildDefinedColumnsAndQuery( - columns, - items, - this.escapeIdentifier.bind(this), - ); - - const definedColumnCount = definedColumns.length; - if (definedColumnCount === 0) { - throw new SyntaxError("Insert needs to have at least one column with a defined value"); - } - const lastDefinedColumnIndex = definedColumnCount - 1; - - query += columnsSql; - if ($isArray(items)) { - const itemsCount = items.length; - const lastItemIndex = itemsCount - 1; - for (let j = 0; j < itemsCount; j++) { - query += "("; - const item = items[j]; - for (let k = 0; k < definedColumnCount; k++) { - const column = definedColumns[k]; - const columnValue = item[column]; - query += `?${k < lastDefinedColumnIndex ? ", " : ""}`; - // If this item has undefined for a column that other items defined, use null - binding_values.push(typeof columnValue === "undefined" ? null : columnValue); - } - if (j < lastItemIndex) { - query += "),"; - } else { - query += ") "; // the user can add RETURNING * or RETURNING id - } - } - } else { - query += "("; - const item = items; - for (let j = 0; j < definedColumnCount; j++) { - const column = definedColumns[j]; - const columnValue = item[column]; - query += `?${j < lastDefinedColumnIndex ? ", " : ""}`; - binding_values.push(columnValue); - } - query += ") "; // the user can add RETURNING * or RETURNING id - } - } else if (command === SQLCommand.in) { - // SELECT * FROM users WHERE id IN (${sql([1, 2, 3])}) - if (!$isArray(items)) { - throw new SyntaxError("An array of values is required for WHERE IN helper"); - } - const itemsCount = items.length; - const lastItemIndex = itemsCount - 1; - query += "("; - for (let j = 0; j < itemsCount; j++) { - query += `?${j < lastItemIndex ? ", " : ""}`; - if (columnCount > 0) { - // we must use a key from a object - if (columnCount > 1) { - // we should not pass multiple columns here - throw new SyntaxError("Cannot use WHERE IN helper with multiple columns"); - } - // SELECT * FROM users WHERE id IN (${sql(users, "id")}) - const value = items[j]; - if (typeof value === "undefined") { - binding_values.push(null); - } else { - const value_from_key = value[columns[0]]; - - if (typeof value_from_key === "undefined") { - binding_values.push(null); - } else { - binding_values.push(value_from_key); - } - } - } else { - const value = items[j]; - if (typeof value === "undefined") { - binding_values.push(null); - } else { - binding_values.push(value); - } - } - } - query += ") "; // more conditions can be added after this - } else { - // UPDATE users SET ${sql({ name: "John", age: 31 })} WHERE id = 1 - let item; - if ($isArray(items)) { - if (items.length > 1) { - throw new SyntaxError("Cannot use array of objects for UPDATE"); - } - item = items[0]; - } else { - item = items; - } - // no need to include if is updateSet or upsert - const isUpsert = query.trimEnd().endsWith("ON DUPLICATE KEY UPDATE"); - if (command === SQLCommand.update && !isUpsert) { - query += " SET "; - } - let hasValues = false; - for (let i = 0; i < columnCount; i++) { - const column = columns[i]; - const columnValue = item[column]; - if (typeof columnValue === "undefined") { - // skip undefined values, this is the expected behavior in JS - continue; - } - hasValues = true; - query += `${this.escapeIdentifier(column)} = ?${i < lastColumnIndex ? ", " : ""}`; - binding_values.push(columnValue); - } - if (query.endsWith(", ")) { - // we got an undefined value at the end, lets remove the last comma - query = query.substring(0, query.length - 2); - } - if (!hasValues) { - throw new SyntaxError("Update needs to have at least one column"); - } - query += " "; // the user can add where clause after this - } - } else { - //TODO: handle sql.array parameters - query += `? `; - if (typeof value === "undefined") { - binding_values.push(null); - } else { - binding_values.push(value); - } - } - } - } else { - throw new SyntaxError("Invalid query: SQL Fragment cannot be executed or was misused"); - } - } - return [query, binding_values]; + isUpsertUpdate(query: string): boolean { + return query.trimEnd().endsWith("ON DUPLICATE KEY UPDATE"); } } export default { MySQLAdapter, - commandToString, - detectCommand, - SQLCommand, }; diff --git a/src/js/internal/sql/postgres.ts b/src/js/internal/sql/postgres.ts index e37da6c6b8f6..c221cb314d8b 100644 --- a/src/js/internal/sql/postgres.ts +++ b/src/js/internal/sql/postgres.ts @@ -1,17 +1,18 @@ import type { PostgresErrorOptions } from "internal/sql/errors"; import type { Query } from "./query"; -import type { ArrayType, DatabaseAdapter, SQLArrayParameter, SQLHelper, SQLResultArray, SSLMode } from "./shared"; +import type { ArrayType, DatabaseAdapter, SQLArrayParameter, SQLCommand, SQLResultArray, SSLMode } from "./shared"; const { - SQLHelper, - SSLMode, SQLResultArray, SQLArrayParameter, - buildDefinedColumnsAndQuery, + BasePooledConnection, + BaseSQLAdapter, + createPooledConnectionHandle, + getHelperCommandFromDetect, + pushBindParam, } = require("internal/sql/shared"); const { - Query, SQLQueryFlags, - symbols: { _strings, _values, _flags, _results, _handle }, + symbols: { _results, _handle }, } = require("internal/sql/query"); function isTypedArray(value: any) { // Buffer should be treated as a normal object @@ -346,330 +347,44 @@ export interface PostgresDotZig { ) => $ZigGeneratedClasses.PostgresSQLQuery; } -const enum SQLCommand { - insert = 0, - update = 1, - updateSet = 2, - where = 3, - in = 4, - none = -1, -} -export type { SQLCommand }; - -function commandToString(command: SQLCommand): string { - switch (command) { - case SQLCommand.insert: - return "INSERT"; - case SQLCommand.updateSet: - case SQLCommand.update: - return "UPDATE"; - case SQLCommand.in: - case SQLCommand.where: - return "WHERE"; - default: - return ""; - } -} - -function detectCommand(query: string): SQLCommand { - const text = query.toLowerCase().trim(); - const text_len = text.length; - - let token = ""; - let command = SQLCommand.none; - let quoted = false; - // we need to reverse search so we find the closest command to the parameter - for (let i = text_len - 1; i >= 0; i--) { - const char = text[i]; - switch (char) { - case " ": // Space - case "\n": // Line feed - case "\t": // Tab character - case "\r": // Carriage return - case "\f": // Form feed - case "\v": { - switch (token) { - case "insert": { - return SQLCommand.insert; - } - case "update": { - return SQLCommand.update; - } - case "where": { - return SQLCommand.where; - } - case "set": { - return SQLCommand.updateSet; - } - case "in": { - return SQLCommand.in; - } - default: { - token = ""; - continue; - } - } - } - default: { - // skip quoted commands - if (char === '"') { - quoted = !quoted; - continue; - } - if (!quoted) { - token = char + token; - } - } - } - } - if (token) { - switch (token) { - case "insert": - return SQLCommand.insert; - case "update": - return SQLCommand.update; - case "where": - return SQLCommand.where; - case "set": - return SQLCommand.updateSet; - case "in": - return SQLCommand.in; - default: - return SQLCommand.none; - } - } - return command; -} - -const enum PooledConnectionState { - pending = 0, - connected = 1, - closed = 2, -} - -const enum PooledConnectionFlags { - /// canBeConnected is used to indicate that at least one time we were able to connect to the database - canBeConnected = 1 << 0, - /// reserved is used to indicate that the connection is currently reserved - reserved = 1 << 1, - /// preReserved is used to indicate that the connection will be reserved in the future when queryCount drops to 0 - preReserved = 1 << 2, -} - -function onQueryFinish(this: PooledPostgresConnection, onClose: (err: Error) => void) { - this.queries.delete(onClose); - this.adapter.release(this); -} - -class PooledPostgresConnection { - private static async createConnection( - options: Bun.SQL.__internal.DefinedPostgresOrMySQLOptions, - onConnected: (err: Error | null, connection: $ZigGeneratedClasses.PostgresSQLConnection) => void, - onClose: (err: Error | null) => void, - ): Promise<$ZigGeneratedClasses.PostgresSQLConnection | null> { - const { - hostname, - port, - username, - tls, - query, - database, - sslMode, - idleTimeout = 0, - connectionTimeout = 30 * 1000, - maxLifetime = 0, - prepare = true, - path, - } = options; - - let password: Bun.MaybePromise | string | undefined | (() => Bun.MaybePromise) = options.password; - - try { - if (typeof password === "function") { - password = password(); - } - - if (password && $isPromise(password)) { - password = await password; - } - - return createPostgresConnection( - hostname, - Number(port), - username || "", - password || "", - database || "", - // > The default value for sslmode is prefer. As is shown in the table, this - // makes no sense from a security point of view, and it only promises - // performance overhead if possible. It is only provided as the default for - // backward compatibility, and is not recommended in secure deployments. - sslMode || SSLMode.disable, - tls || null, - query || "", - path || "", - onConnected, - onClose, - idleTimeout, - connectionTimeout, - maxLifetime, - !prepare, - ); - } catch (e) { - onClose(e as Error); - return null; - } - } - - adapter: PostgresAdapter; - connection: $ZigGeneratedClasses.PostgresSQLConnection | null = null; - state: PooledConnectionState = PooledConnectionState.pending; - storedError: Error | null = null; - queries: Set<(err: Error) => void> = new Set(); - onFinish: ((err: Error | null) => void) | null = null; - connectionInfo: Bun.SQL.__internal.DefinedPostgresOrMySQLOptions; - flags: number = 0; - /// queryCount is used to indicate the number of queries using the connection, if a connection is reserved or if its a transaction queryCount will be 1 independently of the number of queries - queryCount: number = 0; - - #onConnected(err, _) { - if (err) { - err = wrapPostgresError(err); - } - const connectionInfo = this.connectionInfo; - if (connectionInfo?.onconnect) { - connectionInfo.onconnect(err); - } - this.storedError = err; - if (!err) { - this.flags |= PooledConnectionFlags.canBeConnected; - } - this.state = err ? PooledConnectionState.closed : PooledConnectionState.connected; - const onFinish = this.onFinish; - if (onFinish) { - this.queryCount = 0; - this.flags &= ~PooledConnectionFlags.reserved; - this.flags &= ~PooledConnectionFlags.preReserved; - - // pool is closed, lets finish the connection - // pool is closed, lets finish the connection - if (err) { - onFinish(err); - } else { - this.connection?.close(); - } - return; - } - this.adapter.release(this, true); - } - - #onClose(err) { - if (err) { - err = wrapPostgresError(err); - } - const connectionInfo = this.connectionInfo; - if (connectionInfo?.onclose) { - connectionInfo.onclose(err); - } - this.state = PooledConnectionState.closed; - this.connection = null; - this.storedError = err; - - // remove from ready connections if its there - this.adapter.readyConnections?.delete(this); - const queries = new Set(this.queries); - this.queries?.clear?.(); - this.queryCount = 0; - this.flags &= ~PooledConnectionFlags.reserved; - - // notify all queries that the connection is closed - for (const onClose of queries) { - onClose(err); - } - const onFinish = this.onFinish; - if (onFinish) { - onFinish(err); - } - - this.adapter.release(this, true); - } - - constructor(connectionInfo: Bun.SQL.__internal.DefinedPostgresOrMySQLOptions, adapter: PostgresAdapter) { - this.state = PooledConnectionState.pending; - this.adapter = adapter; - this.connectionInfo = connectionInfo; - this.#startConnection(); - } - - async #startConnection() { - this.connection = await PooledPostgresConnection.createConnection( +class PooledPostgresConnection extends BasePooledConnection<$ZigGeneratedClasses.PostgresSQLConnection> { + protected async startConnection() { + this.connection = await createPooledConnectionHandle( + createPostgresConnection, this.connectionInfo, - this.#onConnected.bind(this), - this.#onClose.bind(this), + this.handleConnected.bind(this), + this.handleClose.bind(this), + false, ); } - onClose(onClose: (err: Error) => void) { - this.queries.add(onClose); - } - - bindQuery(query: Query, onClose: (err: Error) => void) { - this.queries.add(onClose); - query.finally(onQueryFinish.bind(this, onClose)); - } - - #doRetry() { - if (this.adapter.closed) { - return; - } - // reset error and state - this.storedError = null; - this.state = PooledConnectionState.pending; - // retry connection - this.#startConnection(); + protected wrapError(error: any): Error { + return wrapPostgresError(error); } - close() { - try { - if (this.state === PooledConnectionState.connected) { - this.connection?.close(); - } - } catch {} - } - flush() { - this.connection?.flush(); - } - retry() { - // if pool is closed, we can't retry - if (this.adapter.closed) { - return false; - } - // we need to reconnect - // lets use a retry strategy - // we can only retry if one day we are able to connect - if (this.flags & PooledConnectionFlags.canBeConnected) { - this.#doRetry(); - } else { - // analyse type of error to see if we can retry - switch (this.storedError?.code) { - case "ERR_POSTGRES_UNSUPPORTED_AUTHENTICATION_METHOD": - case "ERR_POSTGRES_UNKNOWN_AUTHENTICATION_METHOD": - case "ERR_POSTGRES_TLS_NOT_AVAILABLE": - case "ERR_POSTGRES_TLS_UPGRADE_FAILED": - case "ERR_POSTGRES_INVALID_SERVER_SIGNATURE": - case "ERR_POSTGRES_INVALID_SERVER_KEY": - case "ERR_POSTGRES_AUTHENTICATION_FAILED_PBKDF2": - // we can't retry these are authentication errors - return false; - default: - // we can retry - this.#doRetry(); - } + protected isNonRetryableError(code: string | undefined): boolean { + switch (code) { + case "ERR_POSTGRES_UNSUPPORTED_AUTHENTICATION_METHOD": + case "ERR_POSTGRES_UNKNOWN_AUTHENTICATION_METHOD": + case "ERR_POSTGRES_TLS_NOT_AVAILABLE": + case "ERR_POSTGRES_TLS_UPGRADE_FAILED": + case "ERR_POSTGRES_INVALID_SERVER_SIGNATURE": + case "ERR_POSTGRES_INVALID_SERVER_KEY": + case "ERR_POSTGRES_AUTHENTICATION_FAILED_PBKDF2": + // we can't retry these are authentication errors + return true; + default: + return false; } - return true; } } class PostgresAdapter + extends BaseSQLAdapter< + PooledPostgresConnection, + $ZigGeneratedClasses.PostgresSQLConnection, + $ZigGeneratedClasses.PostgresSQLQuery + > implements DatabaseAdapter< PooledPostgresConnection, @@ -677,23 +392,8 @@ class PostgresAdapter $ZigGeneratedClasses.PostgresSQLQuery > { - public readonly connectionInfo: Bun.SQL.__internal.DefinedPostgresOrMySQLOptions; - - public readonly connections: PooledPostgresConnection[]; - public readonly readyConnections: Set; - - public waitingQueue: Array<(err: Error | null, result: any) => void> = []; - public reservedQueue: Array<(err: Error | null, result: any) => void> = []; - - public poolStarted: boolean = false; - public closed: boolean = false; - public totalQueries: number = 0; - public onAllQueriesFinished: (() => void) | null = null; - - constructor(connectionInfo: Bun.SQL.__internal.DefinedPostgresOrMySQLOptions) { - this.connectionInfo = connectionInfo; - this.connections = new Array(connectionInfo.max); - this.readyConnections = new Set(); + protected createPooledConnection(): PooledPostgresConnection { + return new PooledPostgresConnection(this.connectionInfo, this); } escapeIdentifier(str: string) { @@ -720,26 +420,10 @@ class PostgresAdapter code: "ERR_POSTGRES_INVALID_TRANSACTION_STATE", }); } - supportsReservedConnections() { - return true; - } - - getConnectionForQuery(pooledConnection: PooledPostgresConnection) { - return pooledConnection.connection; - } - - attachConnectionCloseHandler(connection: PooledPostgresConnection, handler: () => void): void { - // PostgreSQL pooled connections support onClose handlers - if (connection.onClose) { - connection.onClose(handler); - } - } - - detachConnectionCloseHandler(connection: PooledPostgresConnection, handler: () => void): void { - // PostgreSQL pooled connections track queries - if (connection.queries) { - connection.queries.delete(handler); - } + unsafeTransactionError() { + return new PostgresError("Only use sql.begin, sql.reserved or max: 1", { + code: "ERR_POSTGRES_UNSAFE_TRANSACTION", + }); } array(values: any[], typeNameOrID?: number | ArrayType): SQLArrayParameter { @@ -779,28 +463,6 @@ class PostgresAdapter }; } - validateTransactionOptions(options: string): { valid: boolean; error?: string } { - // The string is interpolated into `BEGIN ${options}`, so refuse anything that - // could terminate the statement or start a new one. - if (!/^[A-Za-z ,]*$/.test(options)) { - return { - valid: false, - error: "Transaction options can only contain letters, spaces, and commas.", - }; - } - return { valid: true }; - } - - validateDistributedTransactionName(name: string): { valid: boolean; error?: string } { - if (name.indexOf("'") !== -1) { - return { - valid: false, - error: "Distributed transaction name cannot contain single quotes.", - }; - } - return { valid: true }; - } - getCommitDistributedSQL(name: string): string { const validation = this.validateDistributedTransactionName(name); if (!validation.valid) { @@ -818,16 +480,7 @@ class PostgresAdapter } createQueryHandle(sql: string, values: unknown[], flags: number) { - if (!(flags & SQLQueryFlags.allowUnsafeTransaction)) { - if (this.connectionInfo.max !== 1) { - const upperCaseSqlString = sql.toUpperCase().trim(); - if (upperCaseSqlString.startsWith("BEGIN") || upperCaseSqlString.startsWith("START TRANSACTION")) { - throw new PostgresError("Only use sql.begin, sql.reserved or max: 1", { - code: "ERR_POSTGRES_UNSAFE_TRANSACTION", - }); - } - } - } + this.checkUnsafeTransaction(sql, flags); return createPostgresQuery( sql, @@ -839,567 +492,23 @@ class PostgresAdapter ); } - maxDistribution() { - if (!this.waitingQueue.length) return 0; - const result = Math.ceil((this.waitingQueue.length + this.totalQueries) / this.connections.length); - return result ? result : 1; + getHelperCommand(query: string): SQLCommand { + return getHelperCommandFromDetect(query, false); } - flushConcurrentQueries() { - const maxDistribution = this.maxDistribution(); - if (maxDistribution === 0) { - return; - } - - while (true) { - const nonReservedConnections = Array.from(this.readyConnections || []).filter( - c => !(c.flags & PooledConnectionFlags.preReserved) && c.queryCount < maxDistribution, - ); - if (nonReservedConnections.length === 0) { - return; - } - const orderedConnections = nonReservedConnections.sort((a, b) => a.queryCount - b.queryCount); - for (const connection of orderedConnections) { - const pending = this.waitingQueue.shift(); - if (!pending) { - return; - } - connection.queryCount++; - this.totalQueries++; - pending(null, connection); - } - } + placeholder(index: number): string { + return "$" + index; } - release(connection: PooledPostgresConnection, connectingEvent: boolean = false) { - if (!connectingEvent) { - connection.queryCount--; - this.totalQueries--; - } - const currentQueryCount = connection.queryCount; - if (currentQueryCount == 0) { - connection.flags &= ~PooledConnectionFlags.reserved; - connection.flags &= ~PooledConnectionFlags.preReserved; + bindParam(value: unknown, binding_values: unknown[], index: number): string { + if (value instanceof SQLArrayParameter) { + binding_values.push(value.serializedValues); + return `$${index}::${value.arrayType}[] `; } - if (this.onAllQueriesFinished) { - // we are waiting for all queries to finish, lets check if we can call it - if (!this.hasPendingQueries()) { - this.onAllQueriesFinished(); - } - } - - if (connection.state !== PooledConnectionState.connected) { - // connection is not ready - if (connection.storedError) { - // this connection got a error but maybe we can wait for another - - if (this.hasConnectionsAvailable()) { - return; - } - - const waitingQueue = this.waitingQueue; - const reservedQueue = this.reservedQueue; - - this.waitingQueue = []; - this.reservedQueue = []; - // we have no connections available so lets fails - for (const pending of waitingQueue) { - pending(connection.storedError, connection); - } - for (const pending of reservedQueue) { - pending(connection.storedError, connection); - } - } - return; - } - - if (currentQueryCount == 0) { - // ok we can actually bind reserved queries to it - const pendingReserved = this.reservedQueue.shift(); - if (pendingReserved) { - connection.flags |= PooledConnectionFlags.reserved; - connection.queryCount++; - this.totalQueries++; - // we have a connection waiting for a reserved connection lets prioritize it - pendingReserved(connection.storedError, connection); - return; - } - } - this.readyConnections.add(connection); - this.flushConcurrentQueries(); - } - - hasConnectionsAvailable() { - if (this.readyConnections?.size > 0) return true; - if (this.poolStarted) { - const pollSize = this.connections.length; - for (let i = 0; i < pollSize; i++) { - const connection = this.connections[i]; - if (connection && connection.state !== PooledConnectionState.closed) { - // some connection is connecting or connected - return true; - } - } - } - return false; - } - - hasPendingQueries() { - if (this.waitingQueue.length > 0 || this.reservedQueue.length > 0) return true; - if (this.poolStarted) { - return this.totalQueries > 0; - } - return false; - } - isConnected() { - if (this.readyConnections?.size > 0) { - return true; - } - if (this.poolStarted) { - const pollSize = this.connections.length; - for (let i = 0; i < pollSize; i++) { - const connection = this.connections[i]; - if (connection.state === PooledConnectionState.connected) { - return true; - } - } - } - return false; - } - flush() { - if (this.closed) { - return; - } - if (this.poolStarted) { - const pollSize = this.connections.length; - for (let i = 0; i < pollSize; i++) { - const connection = this.connections[i]; - if (connection.state === PooledConnectionState.connected) { - connection.connection?.flush(); - } - } - } - } - - async #close() { - let pending; - while ((pending = this.waitingQueue.shift())) { - pending(this.connectionClosedError(), null); - } - while (this.reservedQueue.length > 0) { - const pendingReserved = this.reservedQueue.shift(); - if (pendingReserved) { - pendingReserved(this.connectionClosedError(), null); - } - } - - const promises: Array> = []; - - if (this.poolStarted) { - this.poolStarted = false; - const pollSize = this.connections.length; - for (let i = 0; i < pollSize; i++) { - const connection = this.connections[i]; - switch (connection.state) { - case PooledConnectionState.pending: - { - const { promise, resolve } = Promise.withResolvers(); - connection.onFinish = resolve; - promises.push(promise); - connection.connection?.close(); - } - break; - - case PooledConnectionState.connected: - { - const { promise, resolve } = Promise.withResolvers(); - connection.onFinish = resolve; - promises.push(promise); - connection.connection?.close(); - } - break; - } - // clean connection reference - // @ts-ignore - this.connections[i] = null; - } - } - - this.readyConnections.clear(); - this.waitingQueue.length = 0; - return Promise.all(promises); - } - - async close(options?: { timeout?: number }): Promise { - if (this.closed) { - return; - } - - let timeout = options?.timeout; - if (timeout) { - timeout = Number(timeout); - if (timeout > 2 ** 31 || timeout < 0 || timeout !== timeout) { - throw $ERR_INVALID_ARG_VALUE("options.timeout", timeout, "must be a non-negative integer less than 2^31"); - } - - this.closed = true; - if (timeout === 0 || !this.hasPendingQueries()) { - // close immediately - await this.#close(); - return; - } - - const { promise, resolve } = Promise.withResolvers(); - const timer = setTimeout(() => { - // timeout is reached, lets close and probably fail some queries - this.#close().finally(resolve); - }, timeout * 1000); - timer.unref(); // dont block the event loop - - this.onAllQueriesFinished = () => { - clearTimeout(timer); - // everything is closed, lets close the pool - this.#close().finally(resolve); - }; - - return promise; - } else { - this.closed = true; - if (!this.hasPendingQueries()) { - // close immediately - await this.#close(); - return; - } - - // gracefully close the pool - const { promise, resolve } = Promise.withResolvers(); - - this.onAllQueriesFinished = () => { - // everything is closed, lets close the pool - this.#close().finally(resolve); - }; - - return promise; - } - } - - /** - * @param {function} onConnected - The callback function to be called when the connection is established. - * @param {boolean} reserved - Whether the connection is reserved, if is reserved the connection will not be released until release is called, if not release will only decrement the queryCount counter - */ - connect(onConnected: (err: Error | null, result: any) => void, reserved: boolean = false) { - if (this.closed) { - return onConnected(this.connectionClosedError(), null); - } - - if (!this.readyConnections || this.readyConnections.size === 0) { - // no connection ready lets make some - let retry_in_progress = false; - let all_closed = true; - let storedError: Error | null = null; - - if (this.poolStarted) { - // we already started the pool - // lets check if some connection is available to retry - const pollSize = this.connections.length; - for (let i = 0; i < pollSize; i++) { - const connection = this.connections[i]; - // we need a new connection and we have some connections that can retry - if (connection.state === PooledConnectionState.closed) { - if (connection.retry()) { - // lets wait for connection to be released - if (!retry_in_progress) { - // avoid adding to the queue twice, we wanna to retry every available pool connection - retry_in_progress = true; - if (reserved) { - // we are not sure what connection will be available so we dont pre reserve - this.reservedQueue.push(onConnected); - } else { - this.waitingQueue.push(onConnected); - } - } - } else { - // we have some error, lets grab it and fail if unable to start a connection - storedError = connection.storedError; - } - } else { - // we have some pending or open connections - all_closed = false; - } - } - if (!all_closed && !retry_in_progress) { - // is possible to connect because we have some working connections, or we are just without network for some reason - // wait for connection to be released or fail - if (reserved) { - // we are not sure what connection will be available so we dont pre reserve - this.reservedQueue.push(onConnected); - } else { - this.waitingQueue.push(onConnected); - } - } else if (!retry_in_progress) { - // impossible to connect or retry - onConnected(storedError ?? this.connectionClosedError(), null); - } - return; - } - // we never started the pool, lets start it - if (reserved) { - this.reservedQueue.push(onConnected); - } else { - this.waitingQueue.push(onConnected); - } - this.poolStarted = true; - const pollSize = this.connections.length; - // pool is always at least 1 connection - const firstConnection = new PooledPostgresConnection(this.connectionInfo, this); - this.connections[0] = firstConnection; - if (reserved) { - firstConnection.flags |= PooledConnectionFlags.preReserved; // lets pre reserve the first connection - } - for (let i = 1; i < pollSize; i++) { - this.connections[i] = new PooledPostgresConnection(this.connectionInfo, this); - } - return; - } - if (reserved) { - let connectionWithLeastQueries: PooledPostgresConnection | null = null; - let leastQueries = Infinity; - for (const connection of this.readyConnections || []) { - if (connection.flags & PooledConnectionFlags.preReserved || connection.flags & PooledConnectionFlags.reserved) - continue; - const queryCount = connection.queryCount; - if (queryCount > 0) { - if (queryCount < leastQueries) { - leastQueries = queryCount; - connectionWithLeastQueries = connection; - } - continue; - } - connection.flags |= PooledConnectionFlags.reserved; - connection.queryCount++; - this.totalQueries++; - this.readyConnections?.delete(connection); - onConnected(null, connection); - return; - } - - if (connectionWithLeastQueries) { - // lets mark the connection with the least queries as preReserved if any - connectionWithLeastQueries.flags |= PooledConnectionFlags.preReserved; - } - - // no connection available to be reserved lets wait for a connection to be released - this.reservedQueue.push(onConnected); - } else { - this.waitingQueue.push(onConnected); - this.flushConcurrentQueries(); - } - } - - normalizeQuery(strings: string | TemplateStringsArray, values: unknown[], binding_idx = 1): [string, unknown[]] { - // This function handles array values in single fields: - // - JSON/JSONB are the only field types that can be arrays themselves, so we serialize them - // - SQL array field types (e.g., INTEGER[], TEXT[]) require the sql.array() helper - // - All other types are handled natively - - if (typeof strings === "string") { - // identifier or unsafe query - return [strings, values || []]; - } - - if (!$isArray(strings)) { - // we should not hit this path - throw new SyntaxError("Invalid query: SQL Fragment cannot be executed or was misused"); - } - - const str_len = strings.length; - if (str_len === 0) { - return ["", []]; - } - - let binding_values: any[] = []; - let query = ""; - - for (let i = 0; i < str_len; i++) { - const string = strings[i]; - - if (typeof string === "string") { - query += string; - - if (values.length > i) { - const value = values[i]; - - if (value instanceof Query) { - const q = value as Query; - const [sub_query, sub_values] = this.normalizeQuery(q[_strings], q[_values], binding_idx); - - query += sub_query; - for (let j = 0; j < sub_values.length; j++) { - binding_values.push(sub_values[j]); - } - binding_idx += sub_values.length; - } else if (value instanceof SQLHelper) { - const command = detectCommand(query); - // only selectIn, insert, update, updateSet are allowed - if (command === SQLCommand.none || command === SQLCommand.where) { - throw new SyntaxError("Helpers are only allowed for INSERT, UPDATE and IN commands"); - } - const { columns, value: items } = value as SQLHelper; - const columnCount = columns.length; - if (columnCount === 0 && command !== SQLCommand.in) { - throw new SyntaxError(`Cannot ${commandToString(command)} with no columns`); - } - const lastColumnIndex = columns.length - 1; - - if (command === SQLCommand.insert) { - // - // insert into users ${sql(users)} or insert into users ${sql(user)} - // - - // Build column list while determining which columns have at least one defined value - const { definedColumns, columnsSql } = buildDefinedColumnsAndQuery( - columns, - items, - this.escapeIdentifier.bind(this), - ); - - const definedColumnCount = definedColumns.length; - if (definedColumnCount === 0) { - throw new SyntaxError("Insert needs to have at least one column with a defined value"); - } - const lastDefinedColumnIndex = definedColumnCount - 1; - - query += columnsSql; - if ($isArray(items)) { - const itemsCount = items.length; - const lastItemIndex = itemsCount - 1; - for (let j = 0; j < itemsCount; j++) { - query += "("; - const item = items[j]; - for (let k = 0; k < definedColumnCount; k++) { - const column = definedColumns[k]; - const columnValue = item[column]; - query += `$${binding_idx++}${k < lastDefinedColumnIndex ? ", " : ""}`; - // If this item has undefined for a column that other items defined, use null - binding_values.push(typeof columnValue === "undefined" ? null : columnValue); - } - if (j < lastItemIndex) { - query += "),"; - } else { - query += ") "; // the user can add RETURNING * or RETURNING id - } - } - } else { - query += "("; - const item = items; - for (let j = 0; j < definedColumnCount; j++) { - const column = definedColumns[j]; - const columnValue = item[column]; - query += `$${binding_idx++}${j < lastDefinedColumnIndex ? ", " : ""}`; - binding_values.push(columnValue); - } - query += ") "; // the user can add RETURNING * or RETURNING id - } - } else if (command === SQLCommand.in) { - // SELECT * FROM users WHERE id IN (${sql([1, 2, 3])}) - if (!$isArray(items)) { - throw new SyntaxError("An array of values is required for WHERE IN helper"); - } - const itemsCount = items.length; - const lastItemIndex = itemsCount - 1; - query += "("; - for (let j = 0; j < itemsCount; j++) { - query += `$${binding_idx++}${j < lastItemIndex ? ", " : ""}`; - if (columnCount > 0) { - // we must use a key from a object - if (columnCount > 1) { - // we should not pass multiple columns here - throw new SyntaxError("Cannot use WHERE IN helper with multiple columns"); - } - // SELECT * FROM users WHERE id IN (${sql(users, "id")}) - const value = items[j]; - if (typeof value === "undefined") { - binding_values.push(null); - } else { - const value_from_key = value[columns[0]]; - - if (typeof value_from_key === "undefined") { - binding_values.push(null); - } else { - binding_values.push(value_from_key); - } - } - } else { - const value = items[j]; - if (typeof value === "undefined") { - binding_values.push(null); - } else { - binding_values.push(value); - } - } - } - query += ") "; // more conditions can be added after this - } else { - // UPDATE users SET ${sql({ name: "John", age: 31 })} WHERE id = 1 - let item; - if ($isArray(items)) { - if (items.length > 1) { - throw new SyntaxError("Cannot use array of objects for UPDATE"); - } - item = items[0]; - } else { - item = items; - } - // no need to include if is updateSet - if (command === SQLCommand.update) { - query += " SET "; - } - let hasValues = false; - for (let i = 0; i < columnCount; i++) { - const column = columns[i]; - const columnValue = item[column]; - if (typeof columnValue === "undefined") { - // skip undefined values, this is the expected behavior in JS - continue; - } - hasValues = true; - query += `${this.escapeIdentifier(column)} = $${binding_idx++}${i < lastColumnIndex ? ", " : ""}`; - binding_values.push(columnValue); - } - if (query.endsWith(", ")) { - // we got an undefined value at the end, lets remove the last comma - query = query.substring(0, query.length - 2); - } - if (!hasValues) { - throw new SyntaxError("Update needs to have at least one column"); - } - // the user can add where clause after this - query += " "; - } - } else if (value instanceof SQLArrayParameter) { - query += `$${binding_idx++}::${value.arrayType}[] `; - binding_values.push(value.serializedValues); - } else { - query += `$${binding_idx++} `; - if (typeof value === "undefined") { - binding_values.push(null); - } else { - binding_values.push(value); - } - } - } - } else { - throw new SyntaxError("Invalid query: SQL Fragment cannot be executed or was misused"); - } - } - - return [query, binding_values]; + return pushBindParam(this, value, binding_values, index); } } export default { PostgresAdapter, - SQLCommand, - commandToString, - detectCommand, }; diff --git a/src/js/internal/sql/shared.ts b/src/js/internal/sql/shared.ts index 51301ef8c57b..bd14033348f8 100644 --- a/src/js/internal/sql/shared.ts +++ b/src/js/internal/sql/shared.ts @@ -1,4 +1,11 @@ +import type { Query as QueryType } from "./query"; + const PublicArray = globalThis.Array; +const { + Query, + SQLQueryFlags, + symbols: { _strings, _values }, +} = require("internal/sql/query"); declare global { interface NumberConstructor { @@ -218,6 +225,1037 @@ function buildDefinedColumnsAndQuery( return { definedColumns, columnsSql }; } +const enum SQLCommand { + insert = 0, + update = 1, + updateSet = 2, + where = 3, + in = 4, + none = -1, +} +export type { SQLCommand }; + +function commandToString(command: SQLCommand): string { + switch (command) { + case SQLCommand.insert: + return "INSERT"; + case SQLCommand.updateSet: + case SQLCommand.update: + return "UPDATE"; + case SQLCommand.in: + case SQLCommand.where: + return "WHERE"; + default: + return ""; + } +} + +function detectCommand(query: string, anyAndAllMeanIn: boolean): SQLCommand { + const text = query.toLowerCase().trim(); + const text_len = text.length; + + let token = ""; + let command = SQLCommand.none; + let quoted = false; + // we need to reverse search so we find the closest command to the parameter + for (let i = text_len - 1; i >= 0; i--) { + const char = text[i]; + switch (char) { + case " ": // Space + case "\n": // Line feed + case "\t": // Tab character + case "\r": // Carriage return + case "\f": // Form feed + case "\v": { + switch (token) { + case "insert": { + return SQLCommand.insert; + } + case "update": { + return SQLCommand.update; + } + case "where": { + return SQLCommand.where; + } + case "set": { + return SQLCommand.updateSet; + } + case "in": { + return SQLCommand.in; + } + default: { + token = ""; + continue; + } + } + } + default: { + // skip quoted commands + if (char === '"') { + quoted = !quoted; + continue; + } + if (!quoted) { + token = char + token; + } + } + } + } + if (token) { + switch (token) { + case "insert": + return SQLCommand.insert; + case "update": + return SQLCommand.update; + case "where": + return SQLCommand.where; + case "set": + return SQLCommand.updateSet; + case "in": + return SQLCommand.in; + case "any": + case "all": + // MySQL treats a leading ANY/ALL token like IN; Postgres does not. + return anyAndAllMeanIn ? SQLCommand.in : SQLCommand.none; + default: + return SQLCommand.none; + } + } + return command; +} + +function getHelperCommandFromDetect(query: string, anyAndAllMeanIn: boolean): SQLCommand { + const command = detectCommand(query, anyAndAllMeanIn); + // only selectIn, insert, update, updateSet are allowed + if (command === SQLCommand.none || command === SQLCommand.where) { + throw new SyntaxError("Helpers are only allowed for INSERT, UPDATE and IN commands"); + } + return command; +} + +/** + * The driver-specific hooks consumed by the shared {@link normalizeQuery}. + * Methods stay on the adapter prototype so per-query cost is a monomorphic + * method call. + */ +interface QueryNormalizationAdapter { + escapeIdentifier(name: string): string; + /** Returns the placeholder for the given 1-based binding index ("?" or "$N"). */ + placeholder(index: number): string; + /** Pushes a plain bound value and returns its SQL fragment (always consumes one binding index). */ + bindParam(value: unknown, binding_values: unknown[], index: number): string; + /** Detects the SQL command preceding a helper, throwing if helpers are not allowed there. */ + getHelperCommand(query: string): SQLCommand; + /** Whether the UPDATE helper should omit the SET keyword (MySQL upsert). */ + isUpsertUpdate(query: string): boolean; + throwIfUpdateEmpty(query: string, hasValues: boolean): void; +} + +function pushBindParam( + adapter: QueryNormalizationAdapter, + value: unknown, + binding_values: unknown[], + index: number, +): string { + if (typeof value === "undefined") { + binding_values.push(null); + } else { + binding_values.push(value); + } + return adapter.placeholder(index) + " "; +} + +// This function handles array values in single fields: +// - JSON/JSONB are the only field types that can be arrays themselves, so we serialize them +// - SQL array field types (e.g., INTEGER[], TEXT[]) require the sql.array() helper +// - All other types are handled natively +function normalizeQuery( + adapter: QueryNormalizationAdapter, + strings: string | TemplateStringsArray, + values: unknown[], + binding_idx = 1, +): [string, unknown[]] { + if (typeof strings === "string") { + // identifier or unsafe query + return [strings, values || []]; + } + + if (!$isArray(strings)) { + // we should not hit this path + throw new SyntaxError("Invalid query: SQL Fragment cannot be executed or was misused"); + } + + const str_len = strings.length; + if (str_len === 0) { + return ["", []]; + } + + let binding_values: any[] = []; + let query = ""; + + for (let i = 0; i < str_len; i++) { + const string = strings[i]; + + if (typeof string === "string") { + query += string; + + if (values.length > i) { + const value = values[i]; + + if (value instanceof Query) { + const q = value as QueryType; + const [sub_query, sub_values] = normalizeQuery(adapter, q[_strings], q[_values], binding_idx); + + query += sub_query; + for (let j = 0; j < sub_values.length; j++) { + binding_values.push(sub_values[j]); + } + binding_idx += sub_values.length; + } else if (value instanceof SQLHelper) { + const command = adapter.getHelperCommand(query); + const { columns, value: items } = value as SQLHelper; + const columnCount = columns.length; + if (columnCount === 0 && command !== SQLCommand.in) { + throw new SyntaxError(`Cannot ${commandToString(command)} with no columns`); + } + const lastColumnIndex = columns.length - 1; + + if (command === SQLCommand.insert) { + // + // insert into users ${sql(users)} or insert into users ${sql(user)} + // + + // Build column list while determining which columns have at least one defined value + const { definedColumns, columnsSql } = buildDefinedColumnsAndQuery( + columns, + items, + adapter.escapeIdentifier.bind(adapter), + ); + + const definedColumnCount = definedColumns.length; + if (definedColumnCount === 0) { + throw new SyntaxError("Insert needs to have at least one column with a defined value"); + } + const lastDefinedColumnIndex = definedColumnCount - 1; + + query += columnsSql; + if ($isArray(items)) { + const itemsCount = items.length; + const lastItemIndex = itemsCount - 1; + for (let j = 0; j < itemsCount; j++) { + query += "("; + const item = items[j]; + for (let k = 0; k < definedColumnCount; k++) { + const column = definedColumns[k]; + const columnValue = item[column]; + query += `${adapter.placeholder(binding_idx++)}${k < lastDefinedColumnIndex ? ", " : ""}`; + // If this item has undefined for a column that other items defined, use null + binding_values.push(typeof columnValue === "undefined" ? null : columnValue); + } + if (j < lastItemIndex) { + query += "),"; + } else { + query += ") "; // the user can add RETURNING * or RETURNING id + } + } + } else { + query += "("; + const item = items; + for (let j = 0; j < definedColumnCount; j++) { + const column = definedColumns[j]; + const columnValue = item[column]; + query += `${adapter.placeholder(binding_idx++)}${j < lastDefinedColumnIndex ? ", " : ""}`; + binding_values.push(columnValue); + } + query += ") "; // the user can add RETURNING * or RETURNING id + } + } else if (command === SQLCommand.in) { + // SELECT * FROM users WHERE id IN (${sql([1, 2, 3])}) + if (!$isArray(items)) { + throw new SyntaxError("An array of values is required for WHERE IN helper"); + } + const itemsCount = items.length; + const lastItemIndex = itemsCount - 1; + query += "("; + for (let j = 0; j < itemsCount; j++) { + query += `${adapter.placeholder(binding_idx++)}${j < lastItemIndex ? ", " : ""}`; + if (columnCount > 0) { + // we must use a key from a object + if (columnCount > 1) { + // we should not pass multiple columns here + throw new SyntaxError("Cannot use WHERE IN helper with multiple columns"); + } + // SELECT * FROM users WHERE id IN (${sql(users, "id")}) + const value = items[j]; + if (typeof value === "undefined") { + binding_values.push(null); + } else { + const value_from_key = value[columns[0]]; + + if (typeof value_from_key === "undefined") { + binding_values.push(null); + } else { + binding_values.push(value_from_key); + } + } + } else { + const value = items[j]; + if (typeof value === "undefined") { + binding_values.push(null); + } else { + binding_values.push(value); + } + } + } + query += ") "; // more conditions can be added after this + } else { + // UPDATE users SET ${sql({ name: "John", age: 31 })} WHERE id = 1 + let item; + if ($isArray(items)) { + if (items.length > 1) { + throw new SyntaxError("Cannot use array of objects for UPDATE"); + } + item = items[0]; + } else { + item = items; + } + // no need to include SET if is updateSet or upsert + if (command === SQLCommand.update && !adapter.isUpsertUpdate(query)) { + query += " SET "; + } + let hasValues = false; + for (let i = 0; i < columnCount; i++) { + const column = columns[i]; + const columnValue = item[column]; + if (typeof columnValue === "undefined") { + // skip undefined values, this is the expected behavior in JS + continue; + } + hasValues = true; + query += `${adapter.escapeIdentifier(column as string)} = ${adapter.placeholder(binding_idx++)}${i < lastColumnIndex ? ", " : ""}`; + binding_values.push(columnValue); + } + if (query.endsWith(", ")) { + // we got an undefined value at the end, lets remove the last comma + query = query.substring(0, query.length - 2); + } + adapter.throwIfUpdateEmpty(query, hasValues); + // the user can add where clause after this + query += " "; + } + } else { + query += adapter.bindParam(value, binding_values, binding_idx++); + } + } + } else { + throw new SyntaxError("Invalid query: SQL Fragment cannot be executed or was misused"); + } + } + + return [query, binding_values]; +} + +const enum PooledConnectionState { + pending = 0, + connected = 1, + closed = 2, +} + +const enum PooledConnectionFlags { + /// canBeConnected is used to indicate that at least one time we were able to connect to the database + canBeConnected = 1 << 0, + /// reserved is used to indicate that the connection is currently reserved + reserved = 1 << 1, + /// preReserved is used to indicate that the connection will be reserved in the future when queryCount drops to 0 + preReserved = 1 << 2, +} +export type { PooledConnectionState }; + +function onQueryFinish(this: BasePooledConnection, onClose: (err: Error) => void) { + this.queries.delete(onClose); + this.adapter.release(this); +} + +abstract class BasePooledConnection { + adapter: BaseSQLAdapter; + connection: ConnectionHandle | null = null; + state: PooledConnectionState = PooledConnectionState.pending; + storedError: Error | null = null; + queries: Set<(err: Error) => void> = new Set(); + onFinish: ((err: Error | null) => void) | null = null; + connectionInfo: Bun.SQL.__internal.DefinedPostgresOrMySQLOptions; + flags: number = 0; + /// queryCount is used to indicate the number of queries using the connection, if a connection is reserved or if its a transaction queryCount will be 1 independently of the number of queries + queryCount: number = 0; + + constructor( + connectionInfo: Bun.SQL.__internal.DefinedPostgresOrMySQLOptions, + adapter: BaseSQLAdapter, + ) { + this.adapter = adapter; + this.connectionInfo = connectionInfo; + this.startConnection(); + } + + /** Starts (or restarts) the driver-specific native connection. */ + protected abstract startConnection(): void; + /** Wraps a driver error options object into the driver's Error class. */ + protected abstract wrapError(error: any): Error; + /** Whether the given error code is an authentication-style error that retrying cannot fix. */ + protected abstract isNonRetryableError(code: string | undefined): boolean; + + protected handleConnected(err: any) { + if (err) { + err = this.wrapError(err); + } + const connectionInfo = this.connectionInfo; + if (connectionInfo?.onconnect) { + connectionInfo.onconnect(err); + } + this.storedError = err; + if (!err) { + this.flags |= PooledConnectionFlags.canBeConnected; + } + this.state = err ? PooledConnectionState.closed : PooledConnectionState.connected; + const onFinish = this.onFinish; + if (onFinish) { + this.queryCount = 0; + this.flags &= ~PooledConnectionFlags.reserved; + this.flags &= ~PooledConnectionFlags.preReserved; + + // pool is closed, lets finish the connection + if (err) { + onFinish(err); + } else { + this.connection?.close(); + } + return; + } + this.adapter.release(this, true); + } + + protected handleClose(err: any) { + if (err) { + err = this.wrapError(err); + } + const connectionInfo = this.connectionInfo; + if (connectionInfo?.onclose) { + connectionInfo.onclose(err); + } + this.state = PooledConnectionState.closed; + this.connection = null; + this.storedError = err; + + // remove from ready connections if its there + this.adapter.readyConnections.delete(this); + const queries = new Set(this.queries); + this.queries?.clear?.(); + this.queryCount = 0; + this.flags &= ~PooledConnectionFlags.reserved; + + // notify all queries that the connection is closed + for (const onClose of queries) { + onClose(err); + } + const onFinish = this.onFinish; + if (onFinish) { + onFinish(err); + } + + this.adapter.release(this, true); + } + + onClose(onClose: (err: Error) => void) { + this.queries.add(onClose); + } + + bindQuery(query: QueryType, onClose: (err: Error) => void) { + this.queries.add(onClose); + query.finally(onQueryFinish.bind(this, onClose)); + } + + protected doRetry() { + if (this.adapter.closed) { + return; + } + // reset error and state + this.storedError = null; + this.state = PooledConnectionState.pending; + // retry connection + this.startConnection(); + } + close() { + try { + if (this.state === PooledConnectionState.connected) { + this.connection?.close(); + } + } catch {} + } + flush() { + this.connection?.flush(); + } + retry() { + // if pool is closed, we can't retry + if (this.adapter.closed) { + return false; + } + // we need to reconnect + // lets use a retry strategy + + // we can only retry if one day we are able to connect + if (this.flags & PooledConnectionFlags.canBeConnected) { + this.doRetry(); + } else if (this.isNonRetryableError((this.storedError as any)?.code)) { + // we can't retry these are authentication errors + return false; + } else { + // we can retry + this.doRetry(); + } + return true; + } +} + +function closeNT(onClose: (err: Error) => void, err: Error | null) { + onClose(err as Error); +} + +/** + * Resolves the password (which may be a function and/or a promise) and calls + * the driver's native createConnection with the normalized pool options. + * Extra trailing arguments past `useUnnamedPreparedStatements` (MySQL's + * `allowPublicKeyRetrieval`) are ignored by drivers that don't take them. + */ +async function createPooledConnectionHandle( + nativeCreateConnection: (...args: any[]) => ConnectionHandle, + options: Bun.SQL.__internal.DefinedPostgresOrMySQLOptions, + onConnected: (err: Error | null, connection: ConnectionHandle) => void, + onClose: (err: Error | null) => void, + // MySQL defers synchronous creation failures to the next tick; Postgres + // reports them synchronously. Each driver keeps its pre-existing timing. + deferSyncCloseError: boolean, +): Promise { + const { + hostname, + port, + username, + tls, + query, + database, + sslMode, + idleTimeout = 0, + connectionTimeout = 30 * 1000, + maxLifetime = 0, + prepare = true, + path, + allowPublicKeyRetrieval = false, + } = options; + + let password: Bun.MaybePromise | string | undefined | (() => Bun.MaybePromise) = options.password; + + try { + if (typeof password === "function") { + password = password(); + } + + if (password && $isPromise(password)) { + password = await password; + } + + return nativeCreateConnection( + hostname, + Number(port), + username || "", + password || "", + database || "", + // > The default value for sslmode is prefer. As is shown in the table, this + // makes no sense from a security point of view, and it only promises + // performance overhead if possible. It is only provided as the default for + // backward compatibility, and is not recommended in secure deployments. + sslMode || SSLMode.disable, + tls || null, + query || "", + path || "", + onConnected, + onClose, + idleTimeout, + connectionTimeout, + maxLifetime, + !prepare, + !!allowPublicKeyRetrieval, + ); + } catch (e) { + if (deferSyncCloseError) { + process.nextTick(closeNT, onClose, e); + } else { + onClose(e as Error); + } + return null; + } +} + +abstract class BaseSQLAdapter + implements DatabaseAdapter +{ + public readonly connectionInfo: Bun.SQL.__internal.DefinedPostgresOrMySQLOptions; + + public readonly connections: PooledConnection[]; + public readonly readyConnections: Set = new Set(); + + public waitingQueue: Array<(err: Error | null, result: any) => void> = []; + public reservedQueue: Array<(err: Error | null, result: any) => void> = []; + + public poolStarted: boolean = false; + public closed: boolean = false; + public totalQueries: number = 0; + public onAllQueriesFinished: (() => void) | null = null; + + constructor(connectionInfo: Bun.SQL.__internal.DefinedPostgresOrMySQLOptions) { + this.connectionInfo = connectionInfo; + this.connections = new Array(connectionInfo.max); + } + + protected abstract createPooledConnection(): PooledConnection; + abstract createQueryHandle(sql: string, values: unknown[], flags: number): QueryHandle; + abstract array(values: any[], typeNameOrID?: number | ArrayType): SQLArrayParameter; + abstract getTransactionCommands(options?: string): TransactionCommands; + abstract getDistributedTransactionCommands(name: string): TransactionCommands | null; + abstract getCommitDistributedSQL(name: string): string; + abstract getRollbackDistributedSQL(name: string): string; + abstract escapeIdentifier(name: string): string; + abstract connectionClosedError(): Error; + abstract notTaggedCallError(): Error; + abstract queryCancelledError(): Error; + abstract invalidTransactionStateError(message: string): Error; + abstract unsafeTransactionError(): Error; + abstract getHelperCommand(query: string): SQLCommand; + + placeholder(_index: number): string { + return "?"; + } + + bindParam(value: unknown, binding_values: unknown[], index: number): string { + return pushBindParam(this, value, binding_values, index); + } + + isUpsertUpdate(_query: string): boolean { + return false; + } + + throwIfUpdateEmpty(_query: string, hasValues: boolean): void { + if (!hasValues) { + throw new SyntaxError("Update needs to have at least one column"); + } + } + + normalizeQuery(strings: string | TemplateStringsArray, values: unknown[], binding_idx = 1): [string, unknown[]] { + return normalizeQuery(this, strings, values, binding_idx); + } + + protected checkUnsafeTransaction(sql: string, flags: number) { + if (!(flags & SQLQueryFlags.allowUnsafeTransaction)) { + if (this.connectionInfo.max !== 1) { + const upperCaseSqlString = sql.toUpperCase().trim(); + if (upperCaseSqlString.startsWith("BEGIN") || upperCaseSqlString.startsWith("START TRANSACTION")) { + throw this.unsafeTransactionError(); + } + } + } + } + + supportsReservedConnections() { + return true; + } + + getConnectionForQuery(pooledConnection: PooledConnection) { + return pooledConnection.connection; + } + + attachConnectionCloseHandler(connection: PooledConnection, handler: () => void): void { + if (connection.onClose) { + connection.onClose(handler); + } + } + + detachConnectionCloseHandler(connection: PooledConnection, handler: () => void): void { + if (connection.queries) { + connection.queries.delete(handler); + } + } + + validateTransactionOptions(options: string): { valid: boolean; error?: string } { + // The string is interpolated into the BEGIN/START TRANSACTION statement, so refuse + // anything that could terminate the statement or start a new one. + if (!/^[A-Za-z ,]*$/.test(options)) { + return { + valid: false, + error: "Transaction options can only contain letters, spaces, and commas.", + }; + } + return { valid: true }; + } + + validateDistributedTransactionName(name: string): { valid: boolean; error?: string } { + if (name.indexOf("'") !== -1) { + return { + valid: false, + error: "Distributed transaction name cannot contain single quotes.", + }; + } + return { valid: true }; + } + + maxDistribution() { + if (!this.waitingQueue.length) return 0; + const result = Math.ceil((this.waitingQueue.length + this.totalQueries) / this.connections.length); + return result ? result : 1; + } + + flushConcurrentQueries() { + const maxDistribution = this.maxDistribution(); + if (maxDistribution === 0) { + return; + } + + while (true) { + const nonReservedConnections = Array.from(this.readyConnections).filter( + c => !(c.flags & PooledConnectionFlags.preReserved) && c.queryCount < maxDistribution, + ); + if (nonReservedConnections.length === 0) { + return; + } + const orderedConnections = nonReservedConnections.sort((a, b) => a.queryCount - b.queryCount); + for (const connection of orderedConnections) { + const pending = this.waitingQueue.shift(); + if (!pending) { + return; + } + connection.queryCount++; + this.totalQueries++; + pending(null, connection); + } + } + } + + release(connection: PooledConnection, connectingEvent: boolean = false) { + if (!connectingEvent) { + connection.queryCount--; + this.totalQueries--; + } + const currentQueryCount = connection.queryCount; + if (currentQueryCount == 0) { + connection.flags &= ~PooledConnectionFlags.reserved; + connection.flags &= ~PooledConnectionFlags.preReserved; + } + if (this.onAllQueriesFinished) { + // we are waiting for all queries to finish, lets check if we can call it + if (!this.hasPendingQueries()) { + this.onAllQueriesFinished(); + } + } + + if (connection.state !== PooledConnectionState.connected) { + // connection is not ready + if (connection.storedError) { + // this connection got a error but maybe we can wait for another + + if (this.hasConnectionsAvailable()) { + return; + } + + const waitingQueue = this.waitingQueue; + const reservedQueue = this.reservedQueue; + + this.waitingQueue = []; + this.reservedQueue = []; + // we have no connections available so lets fails + for (const pending of waitingQueue) { + pending(connection.storedError, connection); + } + for (const pending of reservedQueue) { + pending(connection.storedError, connection); + } + } + return; + } + + if (currentQueryCount == 0) { + // ok we can actually bind reserved queries to it + const pendingReserved = this.reservedQueue.shift(); + if (pendingReserved) { + connection.flags |= PooledConnectionFlags.reserved; + connection.queryCount++; + this.totalQueries++; + // we have a connection waiting for a reserved connection lets prioritize it + pendingReserved(connection.storedError, connection); + return; + } + } + this.readyConnections.add(connection); + this.flushConcurrentQueries(); + } + + hasConnectionsAvailable() { + if (this.readyConnections?.size > 0) return true; + if (this.poolStarted) { + const pollSize = this.connections.length; + for (let i = 0; i < pollSize; i++) { + const connection = this.connections[i]; + // The slot can still be an unassigned hole while the pool is starting + // and a synchronous creation failure re-enters via release(). + if (connection && connection.state !== PooledConnectionState.closed) { + // some connection is connecting or connected + return true; + } + } + } + return false; + } + + hasPendingQueries() { + if (this.waitingQueue.length > 0 || this.reservedQueue.length > 0) return true; + if (this.poolStarted) { + return this.totalQueries > 0; + } + return false; + } + isConnected() { + if (this.readyConnections.size > 0) { + return true; + } + if (this.poolStarted) { + const pollSize = this.connections.length; + for (let i = 0; i < pollSize; i++) { + const connection = this.connections[i]; + if (connection.state === PooledConnectionState.connected) { + return true; + } + } + } + return false; + } + flush() { + if (this.closed) { + return; + } + if (this.poolStarted) { + const pollSize = this.connections.length; + for (let i = 0; i < pollSize; i++) { + const connection = this.connections[i]; + if (connection.state === PooledConnectionState.connected) { + connection.connection?.flush(); + } + } + } + } + + async #close() { + let pending; + while ((pending = this.waitingQueue.shift())) { + pending(this.connectionClosedError(), null); + } + while (this.reservedQueue.length > 0) { + const pendingReserved = this.reservedQueue.shift(); + if (pendingReserved) { + pendingReserved(this.connectionClosedError(), null); + } + } + + const promises: Array> = []; + + if (this.poolStarted) { + this.poolStarted = false; + const pollSize = this.connections.length; + for (let i = 0; i < pollSize; i++) { + const connection = this.connections[i]; + switch (connection.state) { + case PooledConnectionState.pending: + case PooledConnectionState.connected: { + const { promise, resolve } = Promise.withResolvers(); + connection.onFinish = resolve; + promises.push(promise); + connection.connection?.close(); + break; + } + } + // clean connection reference + // @ts-ignore + this.connections[i] = null; + } + } + + this.readyConnections.clear(); + this.waitingQueue.length = 0; + return Promise.all(promises); + } + + async close(options?: { timeout?: number }): Promise { + if (this.closed) { + return; + } + + let timeout = options?.timeout; + if (timeout) { + timeout = Number(timeout); + if (timeout > 2 ** 31 || timeout < 0 || timeout !== timeout) { + throw $ERR_INVALID_ARG_VALUE("options.timeout", timeout, "must be a non-negative integer less than 2^31"); + } + + this.closed = true; + if (timeout === 0 || !this.hasPendingQueries()) { + // close immediately + await this.#close(); + return; + } + + const { promise, resolve } = Promise.withResolvers(); + const timer = setTimeout(() => { + // timeout is reached, lets close and probably fail some queries + this.#close().finally(resolve); + }, timeout * 1000); + timer.unref(); // dont block the event loop + + this.onAllQueriesFinished = () => { + clearTimeout(timer); + // everything is closed, lets close the pool + this.#close().finally(resolve); + }; + + return promise; + } else { + this.closed = true; + if (!this.hasPendingQueries()) { + // close immediately + await this.#close(); + return; + } + + // gracefully close the pool + const { promise, resolve } = Promise.withResolvers(); + + this.onAllQueriesFinished = () => { + // everything is closed, lets close the pool + this.#close().finally(resolve); + }; + + return promise; + } + } + + /** + * @param {function} onConnected - The callback function to be called when the connection is established. + * @param {boolean} reserved - Whether the connection is reserved, if is reserved the connection will not be released until release is called, if not release will only decrement the queryCount counter + */ + connect(onConnected: (err: Error | null, result: any) => void, reserved: boolean = false) { + if (this.closed) { + return onConnected(this.connectionClosedError(), null); + } + + if (this.readyConnections.size === 0) { + // no connection ready lets make some + let retry_in_progress = false; + let all_closed = true; + let storedError: Error | null = null; + + if (this.poolStarted) { + // we already started the pool + // lets check if some connection is available to retry + const pollSize = this.connections.length; + for (let i = 0; i < pollSize; i++) { + const connection = this.connections[i]; + // we need a new connection and we have some connections that can retry + if (connection.state === PooledConnectionState.closed) { + if (connection.retry()) { + // lets wait for connection to be released + if (!retry_in_progress) { + // avoid adding to the queue twice, we wanna to retry every available pool connection + retry_in_progress = true; + if (reserved) { + // we are not sure what connection will be available so we dont pre reserve + this.reservedQueue.push(onConnected); + } else { + this.waitingQueue.push(onConnected); + } + } + } else { + // we have some error, lets grab it and fail if unable to start a connection + storedError = connection.storedError; + } + } else { + // we have some pending or open connections + all_closed = false; + } + } + if (!all_closed && !retry_in_progress) { + // is possible to connect because we have some working connections, or we are just without network for some reason + // wait for connection to be released or fail + if (reserved) { + // we are not sure what connection will be available so we dont pre reserve + this.reservedQueue.push(onConnected); + } else { + this.waitingQueue.push(onConnected); + } + } else if (!retry_in_progress) { + // impossible to connect or retry + onConnected(storedError ?? this.connectionClosedError(), null); + } + return; + } + // we never started the pool, lets start it + if (reserved) { + this.reservedQueue.push(onConnected); + } else { + this.waitingQueue.push(onConnected); + } + this.poolStarted = true; + const pollSize = this.connections.length; + // pool is always at least 1 connection + const firstConnection = this.createPooledConnection(); + this.connections[0] = firstConnection; + if (reserved) { + firstConnection.flags |= PooledConnectionFlags.preReserved; // lets pre reserve the first connection + } + for (let i = 1; i < pollSize; i++) { + this.connections[i] = this.createPooledConnection(); + } + return; + } + if (reserved) { + let connectionWithLeastQueries: PooledConnection | null = null; + let leastQueries = Infinity; + for (const connection of this.readyConnections) { + if (connection.flags & PooledConnectionFlags.preReserved || connection.flags & PooledConnectionFlags.reserved) + continue; + const queryCount = connection.queryCount; + if (queryCount > 0) { + if (queryCount < leastQueries) { + leastQueries = queryCount; + connectionWithLeastQueries = connection; + } + continue; + } + connection.flags |= PooledConnectionFlags.reserved; + connection.queryCount++; + this.totalQueries++; + this.readyConnections.delete(connection); + onConnected(null, connection); + return; + } + + if (connectionWithLeastQueries) { + // lets mark the connection with the least queries as preReserved if any + connectionWithLeastQueries.flags |= PooledConnectionFlags.preReserved; + } + + // no connection available to be reserved lets wait for a connection to be released + this.reservedQueue.push(onConnected); + } else { + this.waitingQueue.push(onConnected); + this.flushConcurrentQueries(); + } + } +} + const SQLITE_MEMORY = ":memory:"; const SQLITE_MEMORY_VARIANTS: string[] = [":memory:", "sqlite://:memory:", "sqlite:memory"]; @@ -975,6 +2013,12 @@ export default { normalizeSSLMode, SQLResultArray, SQLArrayParameter, + getHelperCommandFromDetect, + pushBindParam, + normalizeQuery, + BasePooledConnection, + BaseSQLAdapter, + createPooledConnectionHandle, // @ts-expect-error we're exporting a const enum which works in our builtins // generator but not in typescript officially SSLMode, diff --git a/src/js/internal/sql/sqlite.ts b/src/js/internal/sql/sqlite.ts index a7061616bd30..4b2c0f14fb51 100644 --- a/src/js/internal/sql/sqlite.ts +++ b/src/js/internal/sql/sqlite.ts @@ -1,13 +1,16 @@ import type * as BunSQLiteModule from "bun:sqlite"; import type { BaseQueryHandle, Query, SQLQueryResultMode } from "./query"; -import type { ArrayType, DatabaseAdapter, OnConnected, SQLArrayParameter, SQLHelper, SQLResultArray } from "./shared"; - -const { SQLHelper, SQLResultArray, buildDefinedColumnsAndQuery } = require("internal/sql/shared"); -const { - Query, - SQLQueryResultMode, - symbols: { _strings, _values }, -} = require("internal/sql/query"); +import type { + ArrayType, + DatabaseAdapter, + OnConnected, + SQLCommand as SharedSQLCommand, + SQLArrayParameter, + SQLResultArray, +} from "./shared"; + +const { SQLResultArray, normalizeQuery, pushBindParam } = require("internal/sql/shared"); +const { SQLQueryResultMode } = require("internal/sql/query"); const { SQLiteError } = require("internal/sql/errors"); let lazySQLiteModule: typeof BunSQLiteModule; @@ -377,204 +380,38 @@ class SQLiteAdapter implements DatabaseAdapter i) { - const value = values[i]; + // only selectIn, insert, update, updateSet are allowed + if (command === SQLCommand.none || command === SQLCommand.where) { + throw new SyntaxError("Helpers are only allowed for INSERT, UPDATE and WHERE IN commands"); + } + // the local SQLCommand enum is numerically identical to the shared one + return command as unknown as SharedSQLCommand; + } - if (value instanceof Query) { - const q = value as Query; - const [sub_query, sub_values] = this.normalizeQuery(q[_strings], q[_values], binding_idx); + isUpsertUpdate(_query: string): boolean { + return false; + } - query += sub_query; - for (let j = 0; j < sub_values.length; j++) { - binding_values.push(sub_values[j]); - } - binding_idx += sub_values.length; - } else if (value instanceof SQLHelper) { - // when partial is true we stop on the first command we find - const { command } = parseSQLQuery(query, true); - - // only selectIn, insert, update, updateSet are allowed - if (command === SQLCommand.none || command === SQLCommand.where) { - throw new SyntaxError("Helpers are only allowed for INSERT, UPDATE and WHERE IN commands"); - } - const { columns, value: items } = value as SQLHelper; - const columnCount = columns.length; - if (columnCount === 0 && command !== SQLCommand.in) { - throw new SyntaxError(`Cannot ${commandToString(command)} with no columns`); - } - const lastColumnIndex = columns.length - 1; - - if (command === SQLCommand.insert) { - // - // insert into users ${sql(users)} or insert into users ${sql(user)} - // - - // Build column list while determining which columns have at least one defined value - const { definedColumns, columnsSql } = buildDefinedColumnsAndQuery( - columns, - items, - this.escapeIdentifier.bind(this), - ); - - const definedColumnCount = definedColumns.length; - if (definedColumnCount === 0) { - throw new SyntaxError("Insert needs to have at least one column with a defined value"); - } - const lastDefinedColumnIndex = definedColumnCount - 1; - - query += columnsSql; - if ($isArray(items)) { - const itemsCount = items.length; - const lastItemIndex = itemsCount - 1; - for (let j = 0; j < itemsCount; j++) { - query += "("; - const item = items[j]; - for (let k = 0; k < definedColumnCount; k++) { - const column = definedColumns[k]; - const columnValue = item[column]; - // SQLite uses ? for placeholders, not $1, $2, etc. - query += `?${k < lastDefinedColumnIndex ? ", " : ""}`; - // If this item has undefined for a column that other items defined, use null - binding_values.push(typeof columnValue === "undefined" ? null : columnValue); - } - if (j < lastItemIndex) { - query += "),"; - } else { - query += ") "; // the user can add RETURNING * or RETURNING id - } - } - } else { - query += "("; - const item = items; - for (let j = 0; j < definedColumnCount; j++) { - const column = definedColumns[j]; - const columnValue = item[column]; - // SQLite uses ? for placeholders - query += `?${j < lastDefinedColumnIndex ? ", " : ""}`; - binding_values.push(columnValue); - } - query += ") "; // the user can add RETURNING * or RETURNING id - } - } else if (command === SQLCommand.in) { - // SELECT * FROM users WHERE id IN (${sql([1, 2, 3])}) - if (!$isArray(items)) { - throw new SyntaxError("An array of values is required for WHERE IN helper"); - } - const itemsCount = items.length; - const lastItemIndex = itemsCount - 1; - query += "("; - for (let j = 0; j < itemsCount; j++) { - // SQLite uses ? for placeholders - query += `?${j < lastItemIndex ? ", " : ""}`; - if (columnCount > 0) { - // we must use a key from a object - if (columnCount > 1) { - // we should not pass multiple columns here - throw new SyntaxError("Cannot use WHERE IN helper with multiple columns"); - } - // SELECT * FROM users WHERE id IN (${sql(users, "id")}) - const value = items[j]; - if (typeof value === "undefined") { - binding_values.push(null); - } else { - const value_from_key = value[columns[0]]; - - if (typeof value_from_key === "undefined") { - binding_values.push(null); - } else { - binding_values.push(value_from_key); - } - } - } else { - const value = items[j]; - if (typeof value === "undefined") { - binding_values.push(null); - } else { - binding_values.push(value); - } - } - } - query += ") "; // more conditions can be added after this - } else { - // UPDATE users SET ${sql({ name: "John", age: 31 })} WHERE id = 1 - let item; - if ($isArray(items)) { - if (items.length > 1) { - throw new SyntaxError("Cannot use array of objects for UPDATE"); - } - item = items[0]; - } else { - item = items; - } - // no need to include if is updateSet - if (command === SQLCommand.update) { - query += " SET "; - } - for (let i = 0; i < columnCount; i++) { - const column = columns[i]; - const columnValue = item[column]; - if (typeof columnValue === "undefined") { - // skip undefined values, this is the expected behavior in JS - continue; - } - // SQLite uses ? for placeholders - query += `${this.escapeIdentifier(column)} = ?${i < lastColumnIndex ? ", " : ""}`; - if (typeof columnValue === "undefined") { - binding_values.push(null); - } else { - binding_values.push(columnValue); - } - } - if (query.endsWith(", ")) { - // we got an undefined value at the end, lets remove the last comma - query = query.substring(0, query.length - 2); - } - if (query.endsWith("SET ")) { - throw new SyntaxError("Update needs to have at least one column"); - } - // the user can add where clause after this - query += " "; - } - } else { - // SQLite uses ? for placeholders - query += `? `; - if (typeof value === "undefined") { - binding_values.push(null); - } else { - binding_values.push(value); - } - } - } - } else { - throw new SyntaxError("Invalid query: SQL Fragment cannot be executed or was misused"); - } + throwIfUpdateEmpty(query: string, _hasValues: boolean): void { + if (query.endsWith("SET ")) { + throw new SyntaxError("Update needs to have at least one column"); } - - return [query, binding_values]; } connect(onConnected: OnConnected, reserved?: boolean) { diff --git a/src/js/node/_http_server.ts b/src/js/node/_http_server.ts index c7c54c5582ca..76639e5ba4d7 100644 --- a/src/js/node/_http_server.ts +++ b/src/js/node/_http_server.ts @@ -47,6 +47,7 @@ const { setServerIdleTimeout, setServerCustomOptions, getMaxHTTPHeaderSize, + installSocketStubs, } = require("internal/http"); const NumberIsNaN = Number.isNaN; @@ -963,14 +964,6 @@ const NodeHTTPServerSocket = class Socket extends Duplex { return this[kHandle]?.remoteAddress || null; } - get bufferSize() { - return this.writableLength; - } - - connect(_port, _host, _connectListener) { - return this; - } - _destroy(err, callback) { const handle = this[kHandle]; if (!handle) { @@ -1013,10 +1006,6 @@ const NodeHTTPServerSocket = class Socket extends Duplex { return this[kHandle]?.localAddress?.port; } - get pending() { - return this.connecting; - } - #resumeSocket() { const handle = this[kHandle]; const response = handle?.response; @@ -1044,54 +1033,6 @@ const NodeHTTPServerSocket = class Socket extends Duplex { this.#resumeSocket(); } - get readyState() { - if (this.connecting) return "opening"; - if (this.readable) { - return this.writable ? "open" : "readOnly"; - } else { - return this.writable ? "writeOnly" : "closed"; - } - } - - ref() { - return this; - } - - get remoteAddress() { - return this.address()?.address; - } - - set remoteAddress(val) { - // initialize the object so that other properties wouldn't be lost - this.address().address = val; - } - - get remotePort() { - return this.address()?.port; - } - - set remotePort(val) { - // initialize the object so that other properties wouldn't be lost - this.address().port = val; - } - - get remoteFamily() { - return this.address()?.family; - } - - set remoteFamily(val) { - // initialize the object so that other properties wouldn't be lost - this.address().family = val; - } - - resetAndDestroy() {} - - setKeepAlive(_enable = false, _initialDelay = 0) {} - - setNoDelay(_noDelay = true) { - return this; - } - setTimeout(_timeout, _callback) { return this; } @@ -1102,10 +1043,6 @@ const NodeHTTPServerSocket = class Socket extends Duplex { throw err; } - unref() { - return this; - } - _write(_chunk, _encoding, _callback) { const handle = this[kHandle]; // only enable writting if we can drain @@ -1229,6 +1166,7 @@ function _writeHead(statusCode, reason, obj, response) { updateHasBody(response, statusCode); } +installSocketStubs(NodeHTTPServerSocket); Object.defineProperty(NodeHTTPServerSocket, "name", { value: "Socket" }); function ServerResponse(req, options): void { diff --git a/src/js_parser/lexer.rs b/src/js_parser/lexer.rs index 291cbd0ed0a2..c09eab335fac 100644 --- a/src/js_parser/lexer.rs +++ b/src/js_parser/lexer.rs @@ -5,7 +5,6 @@ use core::fmt; use bun_ast as js_ast; use bun_ast::lexer_tables as tables; use bun_ast::{LexerLog, Loc, Log, Range, Source}; -use bun_core::fmt::hex_digit_value_u32; use bun_core::strings; use bun_core::strings::CodepointIterator; use bun_core::{Environment, feature_flags as FeatureFlags}; @@ -448,6 +447,41 @@ impl< } } +impl< + 'a, + const IS_JSON: bool, + const ALLOW_COMMENTS: bool, + const ALLOW_TRAILING_COMMAS: bool, + const IGNORE_LEADING_ESCAPE_SEQUENCES: bool, + const IGNORE_TRAILING_ESCAPE_SEQUENCES: bool, + const JSON_WARN_DUPLICATE_KEYS: bool, + const WAS_ORIGINALLY_MACRO: bool, + const GUESS_INDENTATION: bool, +> bun_ast::lexer_log::EscapeLexer<'a> + for LexerType< + 'a, + IS_JSON, + ALLOW_COMMENTS, + ALLOW_TRAILING_COMMAS, + IGNORE_LEADING_ESCAPE_SEQUENCES, + IGNORE_TRAILING_ESCAPE_SEQUENCES, + JSON_WARN_DUPLICATE_KEYS, + WAS_ORIGINALLY_MACRO, + GUESS_INDENTATION, + > +{ + type Buf = Vec; + const IS_JSON: bool = IS_JSON; + #[inline] + fn end_mut(&mut self) -> &mut usize { + &mut self.end + } + #[inline] + fn push_codepoint(buf: &mut Vec, c: u32) { + strings::push_codepoint_utf16(buf, c); + } +} + lexer_impl_header! { /// Reborrow the shared `Log`. The `&self` receiver lets call sites pass /// other `self.*` fields as arguments without a borrow-checker conflict; @@ -605,344 +639,7 @@ lexer_impl_header! { if IS_JSON { self.is_ascii_only = false; } - - let iterator = CodepointIterator::init(text); - let mut iter = strings::Cursor::default(); - while iterator.next(&mut iter) { - let width = iter.width; - match iter.c { - 0x0D => { - // From the specification: - // - // 11.8.6.1 Static Semantics: TV and TRV - // - // TV excludes the code units of LineContinuation while TRV includes - // them. and LineTerminatorSequences are normalized to - // for both TV and TRV. An explicit EscapeSequence is needed to - // include a or sequence. - - // Convert '\r\n' into '\n' - let next_i: usize = iter.i as usize + 1; - iter.i += (next_i < text.len() && text[next_i] == b'\n') as u32; - - // Convert '\r' into '\n' - buf.push(u16::from(b'\n')); - continue; - } - - 0x5C => { - if !iterator.next(&mut iter) { - return Ok(()); - } - - let c2 = iter.c; - let width2 = iter.width; - match c2 { - // https://mathiasbynens.be/notes/javascript-escapes#single - 0x62 => { - buf.push(0x08); - continue; - } - 0x66 => { - buf.push(0x0C); - continue; - } - 0x6E => { - buf.push(0x0A); - continue; - } - 0x76 => { - // Vertical tab is invalid JSON - // We're going to allow it. - buf.push(0x0B); - continue; - } - 0x74 => { - buf.push(0x09); - continue; - } - 0x72 => { - buf.push(0x0D); - continue; - } - - // legacy octal literals - 0x30..=0x37 => { - let octal_start = - (iter.i as usize + width2 as usize).saturating_sub(2); - if IS_JSON { - self.end = (start + iter.i as usize) - .saturating_sub(width2 as usize); - self.syntax_error()?; - } - - // 1-3 digit octal - let mut is_bad = false; - let mut value: i64 = (c2 - 0x30) as i64; - let mut prev = iter; - - if !iterator.next(&mut iter) { - if value == 0 { - buf.push(0); - return Ok(()); - } - self.syntax_error()?; - return Ok(()); - } - - let c3: CodePoint = iter.c; - - match c3 { - 0x30..=0x37 => { - value = value * 8 + (c3 - 0x30) as i64; - prev = iter; - if !iterator.next(&mut iter) { - return self.syntax_error(); - } - - let c4 = iter.c; - match c4 { - 0x30..=0x37 => { - let temp = - value * 8 + (c4 - 0x30) as i64; - if temp < 256 { - value = temp; - } else { - iter = prev; - } - } - 0x38 | 0x39 => { - is_bad = true; - } - _ => { - iter = prev; - } - } - } - 0x38 | 0x39 => { - is_bad = true; - } - _ => { - iter = prev; - } - } - - iter.c = i32::try_from(value).expect("int cast"); - if is_bad { - // `octal_start` is text-relative like `iter.i`; - // map back to absolute source position the same - // way every sibling error path does (e.g. - // `start + hex_start` in the `\u{}` branch). - self.add_range_error( - Range { - loc: Loc { - start: i32::try_from(start + octal_start).expect("int cast"), - }, - len: i32::try_from( - iter.i as usize - octal_start, - ) - .unwrap(), - }, - format_args!("Invalid legacy octal literal"), - ) - .expect("unreachable"); - } - } - 0x38 | 0x39 => { - iter.c = c2; - } - // 2-digit hexadecimal - 0x78 => { - let mut value: CodePoint = 0; - let mut c3: CodePoint; - let mut width3: u8; - - if !iterator.next(&mut iter) { - return self.syntax_error(); - } - c3 = iter.c; - width3 = iter.width; - match hex_digit_value_u32(c3 as u32) { - Some(d) => value = (value * 16) | d as CodePoint, - None => { - self.end = (start + iter.i as usize) - .saturating_sub(width3 as usize); - return self.syntax_error(); - } - } - - if !iterator.next(&mut iter) { - return self.syntax_error(); - } - c3 = iter.c; - width3 = iter.width; - match hex_digit_value_u32(c3 as u32) { - Some(d) => value = (value * 16) | d as CodePoint, - None => { - self.end = (start + iter.i as usize) - .saturating_sub(width3 as usize); - return self.syntax_error(); - } - } - - iter.c = value; - } - 0x75 => { - // We're going to make this an i64 so we don't risk integer overflows - // when people do weird things - let mut value: i64 = 0; - - if !iterator.next(&mut iter) { - return self.syntax_error(); - } - let mut c3 = iter.c; - let mut width3 = iter.width; - - // variable-length - if c3 == 0x7B { - if IS_JSON { - self.end = (start + iter.i as usize) - .saturating_sub(width2 as usize); - self.syntax_error()?; - } - - // `iter.i` is the byte offset of `{` inside `text`; - // back up past `\` and `u` only. `width3` is the - // width of `{` itself, which `iter.i` already points - // at — subtracting it lands one character too early. - let hex_start = (iter.i as usize) - .saturating_sub(width as usize) - .saturating_sub(width2 as usize); - let mut is_first = true; - let mut is_out_of_range = false; - 'variable_length: loop { - if !iterator.next(&mut iter) { - break 'variable_length; - } - c3 = iter.c; - - if c3 == 0x7D { - if is_first { - self.end = (start + iter.i as usize) - .saturating_sub(width3 as usize); - return self.syntax_error(); - } - break 'variable_length; - } - match hex_digit_value_u32(c3 as u32) { - Some(d) => value = (value * 16) | d as i64, - None => { - self.end = (start + iter.i as usize) - .saturating_sub(width3 as usize); - return self.syntax_error(); - } - } - - // '\U0010FFFF - // copied from golang utf8.MaxRune - if value > 1_114_111 { - is_out_of_range = true; - } - is_first = false; - } - - if is_out_of_range { - self.add_range_error( - Range { - loc: Loc { - start: i32::try_from(start + hex_start) - .unwrap(), - }, - len: i32::try_from( - (iter.i as usize).saturating_sub(hex_start), - ) - .unwrap(), - }, - format_args!( - "Unicode escape sequence is out of range" - ), - )?; - - return Ok(()); - } - - // fixed-length - } else { - // Fixed-length - let mut j: usize = 0; - while j < 4 { - match hex_digit_value_u32(c3 as u32) { - Some(d) => value = (value * 16) | d as i64, - None => { - self.end = (start + iter.i as usize) - .saturating_sub(width3 as usize); - return self.syntax_error(); - } - } - - if j < 3 { - if !iterator.next(&mut iter) { - return self.syntax_error(); - } - c3 = iter.c; - width3 = iter.width; - } - j += 1; - } - let _ = width3; - } - - iter.c = value as CodePoint; // @truncate - } - 0x0D => { - if IS_JSON { - self.end = (start + iter.i as usize) - .saturating_sub(width2 as usize); - self.syntax_error()?; - } - - // Make sure Windows CRLF counts as a single newline - let next_i: usize = iter.i as usize + 1; - iter.i += - (next_i < text.len() && text[next_i] == b'\n') as u32; - - // Ignore line continuations. A line continuation is not an escaped newline. - continue; - } - 0x0A | 0x2028 | 0x2029 => { - if IS_JSON { - self.end = (start + iter.i as usize) - .saturating_sub(width2 as usize); - self.syntax_error()?; - } - - // Ignore line continuations. A line continuation is not an escaped newline. - continue; - } - _ => { - if IS_JSON { - match c2 { - 0x22 | 0x5C | 0x2F => {} - _ => { - self.end = (start + iter.i as usize) - .saturating_sub(width2 as usize); - self.syntax_error()?; - } - } - } - iter.c = c2; - } - } - } - _ => {} - } - - match iter.c { - -1 => return self.add_default_error(b"Unexpected end of file"), - c => strings::push_codepoint_utf16(buf, c as u32), - } - } - Ok(()) + bun_ast::lexer_log::decode_escape_sequences::<_, true, false>(self, start, text, buf) } // PERF: heavy sub-scanner — the per-byte string body loop plus the diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs index f72795f5d205..b276dd1dd815 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -56,6 +56,37 @@ macro_rules! init_p { }}; } +/// `init_p!` plus the shared `&mut self` prologue of `_scan_imports`, +/// `to_lazy_export_ast`, and `analyze`: `Lexer` owns `Vec`s and `Options` +/// owns `jsx: Pragma` boxes, so a bitwise `ptr::read` would double-free when +/// `self` later drops. Move them out, leaving inert placeholders, build the +/// parser in place, and bind `$p` to it. +/// +/// The inert placeholder lexer is given its *own* arena-allocated `Log` +/// (empty `Vec`, arena-leaked) so it does not alias `self.log` at all — +/// keeps the placeholder fully disjoint from the real `Log` handed to `P` +/// and never read again. +macro_rules! take_and_init_p { + (let $p:ident: $ty:ty = $self:ident) => { + let lexer = core::mem::replace( + &mut $self.lexer, + js_lexer::Lexer::init_without_reading( + $self.bump.alloc(bun_ast::Log::default()), + $self.source, + $self.bump, + ), + ); + let options = core::mem::take(&mut $self.options); + // `P.log` and `Lexer.log` are both `NonNull` (see P.rs / lexer.rs + // field docs), so handing the same raw pointer to both is defined — + // no `&mut` is materialized. + let mut __p = init_p!($ty; + $self.bump, $self.log, $self.source, $self.define, lexer, options); + // SAFETY: `init_p!` only yields after `init` succeeded. + let $p: &mut $ty = unsafe { __p.assume_init_mut() }; + }; +} + pub struct Parser<'a> { pub options: Options<'a>, pub lexer: js_lexer::Lexer<'a>, @@ -387,31 +418,7 @@ impl<'a> Parser<'a> { scan_pass: &'a mut ScanPassResult, ) -> Result<(), Error> { type Pi<'a, const TS: bool> = P<'a, TS, true>; - // `Lexer` owns `Vec`s and `Options` owns - // `jsx: Pragma` boxes, so a bitwise `ptr::read` would double-free - // when `self` later drops. Move them out, leaving inert placeholders. - // - // The inert placeholder lexer is given its *own* arena-allocated `Log` - // so it does not alias `self.log` at all — keeps the placeholder fully - // disjoint from the real `Log` handed to `P` and never read again. - let lexer = core::mem::replace( - &mut self.lexer, - js_lexer::Lexer::init_without_reading( - // Disjoint dummy `Log` (empty `Vec`, arena-leaked); the - // placeholder is never read after this point. - self.bump.alloc(bun_ast::Log::default()), - self.source, - self.bump, - ), - ); - let options = core::mem::take(&mut self.options); - // `P.log` and `Lexer.log` are both `NonNull` (see P.rs / lexer.rs - // field docs), so handing the same raw pointer to both is defined — - // no `&mut` is materialized. - let mut __p = init_p!(Pi<'_, TS>; - self.bump, self.log, self.source, self.define, lexer, options); - // SAFETY: `init_p!` only yields after `init` succeeded. - let p: &mut Pi<'_, TS> = unsafe { __p.assume_init_mut() }; + take_and_init_p!(let p: Pi<'_, TS> = self); p.import_records = crate::p::ImportRecordList::Borrowed(&mut scan_pass.import_records); p.named_imports = crate::p::NamedImportsType::Borrowed(&mut scan_pass.named_imports); @@ -531,29 +538,7 @@ impl<'a> Parser<'a> { runtime_api_call: &'static [u8], symbols: js_ast::symbol::List<'a>, ) -> Result, Error> { - // Move lexer/options out and leave inert - // placeholders so `self` may drop without double-free. - // - // The placeholder lexer gets its own arena `Log` so it does not alias - // `self.log` (see `_scan_imports`). - let lexer = core::mem::replace( - &mut self.lexer, - js_lexer::Lexer::init_without_reading( - // Disjoint dummy `Log` (empty `Vec`, arena-leaked); the - // placeholder is never read after this point. - self.bump.alloc(bun_ast::Log::default()), - self.source, - self.bump, - ), - ); - let options = core::mem::take(&mut self.options); - // `P.log` and `Lexer.log` are both `NonNull` (see P.rs / lexer.rs - // field docs), so handing the same raw pointer to both is defined — - // no `&mut` is materialized. - let mut __p = init_p!(JavaScriptParser<'_>; - self.bump, self.log, self.source, self.define, lexer, options); - // SAFETY: `init_p!` only yields after `init` succeeded. - let p: &mut JavaScriptParser<'_> = unsafe { __p.assume_init_mut() }; + take_and_init_p!(let p: JavaScriptParser<'_> = self); // Instead of doing "should_fold_typescript_constant_expressions or features.minify_syntax" // Let's enable this flag file-wide @@ -622,29 +607,7 @@ impl<'a> Parser<'a> { context: *mut c_void, callback: &dyn Fn(*mut c_void, &mut TSXParser, &mut [js_ast::Part]) -> Result<(), Error>, ) -> Result<(), Error> { - // See `_scan_imports`: move lexer/options out, leaving inert - // placeholders so `self` may drop without double-free. - // - // The placeholder lexer gets its own arena `Log` so it does not alias - // `self.log` (see `_scan_imports`). - let lexer = core::mem::replace( - &mut self.lexer, - js_lexer::Lexer::init_without_reading( - // Disjoint dummy `Log` (empty `Vec`, arena-leaked); the - // placeholder is never read after this point. - self.bump.alloc(bun_ast::Log::default()), - self.source, - self.bump, - ), - ); - let options = core::mem::take(&mut self.options); - // `P.log` and `Lexer.log` are both `NonNull` (see P.rs / lexer.rs - // field docs), so handing the same raw pointer to both is defined — - // no `&mut` is materialized. - let mut __p = init_p!(TSXParser<'_>; - self.bump, self.log, self.source, self.define, lexer, options); - // SAFETY: `init_p!` only yields after `init` succeeded. - let p: &mut TSXParser<'_> = unsafe { __p.assume_init_mut() }; + take_and_init_p!(let p: TSXParser<'_> = self); // Consume a leading hashbang comment let mut hashbang: &[u8] = b""; diff --git a/src/js_parser_jsc/Macro.rs b/src/js_parser_jsc/Macro.rs index 7daa679604ec..ce0cf1b8b006 100644 --- a/src/js_parser_jsc/Macro.rs +++ b/src/js_parser_jsc/Macro.rs @@ -596,9 +596,7 @@ impl<'a> Run<'a> { pub fn run(&mut self, value: JSValue) -> Result { use ConsoleObject::formatter::Tag as T; - // `Tag::get` returns `TagResult { tag: TagPayload, .. }`; - // collapse the payload to its discriminant via `.tag()`. - match T::get(value, self.global)?.tag.tag() { + match T::get(value, self.global)?.tag { T::Error => self.coerce(T::Error, value), T::Undefined => self.coerce(T::Undefined, value), T::Null => self.coerce(T::Null, value), diff --git a/src/js_parser_jsc/expr_jsc.rs b/src/js_parser_jsc/expr_jsc.rs index 12e6fb8b3a2b..84bfb46aec0e 100644 --- a/src/js_parser_jsc/expr_jsc.rs +++ b/src/js_parser_jsc/expr_jsc.rs @@ -4,7 +4,7 @@ use bun_ast::{E, Expr, ExprData, G, ToJSError}; use bun_collections::VecExt; -use bun_core::{StackCheck, String as BunString, strings}; +use bun_core::{StackCheck, String as BunString}; use bun_jsc::{JSGlobalObject, JSValue, JsError, bun_string_jsc}; /// Map a `bun_jsc::JsError` into the AST-layer `ToJSError`. Orphan rules forbid @@ -143,20 +143,11 @@ pub(crate) fn object_to_js( Ok(obj) } -/// Serialize UTF-8 bytes to a JS string, transcoding to UTF-16 only when the -/// bytes are not pure ASCII (`to_utf16_alloc` returns `Ok(None)` for -/// pure-ASCII, in which case the 8-bit Latin-1 form is kept). +/// Serialize UTF-8 bytes to a JS string. `createUTF8ForJS` allocates the final +/// WTF string in one pass (Latin-1 for pure-ASCII, UTF-16 with U+FFFD +/// replacement of invalid sequences otherwise). fn utf8_bytes_to_js(bytes: &[u8], global: &JSGlobalObject) -> Result { - let utf16 = strings::to_utf16_alloc(bytes, false, false).map_err(|_| ToJSError::OutOfMemory)?; - if let Some(utf16) = utf16 { - let (mut out, chars) = BunString::create_uninitialized_utf16(utf16.len()); - chars.copy_from_slice(&utf16); - bun_string_jsc::transfer_to_js(&mut out, global).map_err(js_err) - } else { - let (mut out, chars) = BunString::create_uninitialized_latin1(bytes.len()); - chars.copy_from_slice(bytes); - bun_string_jsc::transfer_to_js(&mut out, global).map_err(js_err) - } + bun_string_jsc::create_utf8_for_js(global, bytes).map_err(js_err) } /// `E.String` → JS string conversion. diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index f85dde30ad1d..f5a47288088d 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -21,16 +21,9 @@ use bun_core::strings::CodepointIterator; use bun_options_types::bundle_enums as bundle_opts; use bun_sys::Fd; -/// Local stand-in for `bun_core::Encoding` 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, -} +/// Const-generic-capable encoding enum; canonical impl lives in +/// `bun_core::printer` next to the escaping loop. +pub use bun_core::printer::Encoding; /// Byte-sink trait used by the string-escape helpers and `StdWriterAdapter`. /// Re-exported from `bun_io` (canonical in `bun_core::io`); any `bun_io::Write` @@ -774,37 +767,12 @@ pub mod analyze_transpiled_module { /// link-interface); the printer just holds the raw pointer. pub type RuntimeTranspilerCacheRef = core::ptr::NonNull; -use bun_core::fmt::hex2_upper; // remaining `\xHH` site below -use bun_core::printer::{ - FIRST_ASCII, FIRST_HIGH_SURROGATE, LAST_ASCII, LAST_LOW_SURROGATE, bmp_escape, - surrogate_pair_escape, -}; +use bun_core::printer::{FIRST_ASCII, LAST_ASCII, bmp_escape, surrogate_pair_escape}; /// For support JavaScriptCore const ASCII_ONLY_ALWAYS_ON_UNLESS_MINIFYING: bool = true; -// Callers widen to i32 at the boundary. -// PERF: `ascii_only` is a *runtime* arg so the large -// callers (`write_pre_quoted_string_inner`, `estimate_length_for_utf8`) collapse to a -// single monomorphization instead of one per (ascii_only × quote_char × …) combo — -// see the comment on `write_pre_quoted_string`. -#[inline] -pub fn can_print_without_escape(c: i32, ascii_only: bool) -> bool { - if c <= LAST_ASCII as i32 { - c >= FIRST_ASCII as i32 - && c != i32::from(b'\\') - && c != i32::from(b'"') - && c != i32::from(b'\'') - && c != i32::from(b'`') - && c != i32::from(b'$') - } else { - !ascii_only - && c != 0xFEFF - && c != 0x2028 - && c != 0x2029 - && (c < FIRST_HIGH_SURROGATE as i32 || c > LAST_LOW_SURROGATE as i32) - } -} +pub use bun_core::printer::can_print_without_escape; const INDENTATION_SPACE_BUF: [u8; 128] = [b' '; 128]; const INDENTATION_TAB_BUF: [u8; 128] = [b'\t'; 128]; @@ -965,226 +933,7 @@ where write_pre_quoted_string_inner::(text_in, writer, QUOTE_CHAR, ASCII_ONLY, JSON) } -/// `quote_char` / `ascii_only` / `json` are runtime args (were `const`): the -/// branches on them are cheap and well-predicted, and collapsing the -/// monomorphizations keeps the hot transpile pages dense (see the facade above). -/// `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, -) -> Result<(), bun_core::Error> -where - W: Write + ?Sized, -{ - debug_assert!( - !(json && quote_char != b'"'), - "for json, quote_char must be '\"'" - ); - - // this is a large hot-path function; logic is ported 1:1 but the - // utf16 path needs &[u16] handling. - 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 { - 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 { - 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 - } - 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) - } - }; - - if can_print_without_escape(c, ascii_only) { - 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) - { - let j = j as usize; - writer.write_all(&text[i..i + clamped_width + j])?; - i += clamped_width + j; - } else { - writer.write_all(&text[i..])?; - break; - } - } - 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")?; - i += 1; - } - 0x08 => { - writer.write_all(b"\\b")?; - i += 1; - } - 0x0C => { - writer.write_all(b"\\f")?; - i += 1; - } - 0x0A => { - if quote_char == b'`' { - writer.write_all(b"\n")?; - } else { - writer.write_all(b"\\n")?; - } - i += 1; - } - 0x0D => { - writer.write_all(b"\\r")?; - i += 1; - } - // \v - 0x0B => { - writer.write_all(b"\\v")?; - i += 1; - } - // "\\" - 0x5C => { - writer.write_all(b"\\\\")?; - i += 1; - } - 0x22 => { - if quote_char == b'"' { - writer.write_all(b"\\\"")?; - } else { - writer.write_all(b"\"")?; - } - i += 1; - } - 0x27 => { - if quote_char == b'\'' { - writer.write_all(b"\\'")?; - } else { - writer.write_all(b"'")?; - } - i += 1; - } - 0x60 => { - if quote_char == b'`' { - writer.write_all(b"\\`")?; - } else { - writer.write_all(b"`")?; - } - i += 1; - } - 0x24 => { - if quote_char == b'`' { - let next = if i + clamped_width < n { - Some(code_unit_at!(i + clamped_width)) - } else { - None - }; - if next == Some(b'{' as i32) { - writer.write_all(b"\\$")?; - } else { - writer.write_all(b"$")?; - } - } else { - writer.write_all(b"$")?; - } - i += 1; - } - 0x09 => { - if quote_char == b'`' { - writer.write_all(b"\t")?; - } else { - writer.write_all(b"\\t")?; - } - i += 1; - } - _ => { - 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]])?; - } else if c <= 0xFFFF { - writer.write_all(&bmp_escape(c as u32))?; - } else { - writer.write_all(&surrogate_pair_escape(c as u32))?; - } - } - } - } - Ok(()) -} - -pub fn quote_for_json( - text: &[u8], - bytes: &mut MutableString, - ascii_only: bool, -) -> Result<(), bun_core::Error> { - // `ascii_only` is threaded at runtime so - // the heavy escaper isn't monomorphized per ascii_only/quote-char combo. - // - // Heuristic reservation (~12.5% slack) instead of `estimate_length_for_utf8`, - // which would do a full SIMD scan + per-escape rune decode over `text` just - // to size the buffer — the same work `write_pre_quoted_string_inner` repeats - // immediately below. Tab-indented JS (e.g. three.js) has ~9.4% of bytes - // needing 2-byte escapes (tabs + newlines + quotes/backslashes), so 6.25% - // slack would under-shoot and force a 2x doubling memcpy of the whole - // source. The 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_inner::<_, { Encoding::Utf8 }>(text, bytes, b'"', ascii_only, true)?; - bytes.append_char(b'"').expect("unreachable"); - Ok(()) -} +pub use bun_core::printer::{quote_for_json, write_pre_quoted_string_inner}; pub fn write_json_string( input: &[u8], diff --git a/src/jsc/AsyncModule.rs b/src/jsc/AsyncModule.rs index c2741524f6a5..3a842eb9b53d 100644 --- a/src/jsc/AsyncModule.rs +++ b/src/jsc/AsyncModule.rs @@ -751,6 +751,116 @@ impl AsyncModule { drop(unsafe { bun_core::heap::take(this) }); } + /// Shared builder for the package resolve/download error objects: creates + /// the error instance from `msg` and sets the `url` (when present), + /// `name`, and `pkg` properties. + fn package_error_instance( + global_this: &JSGlobalObject, + msg: &[u8], + name: &[u8], + url: &[u8], + pkg: &[u8], + ) -> JSValue { + let error_instance = ZigString::from_bytes(msg) + .with_encoding() + .to_error_instance(global_this); + if !url.is_empty() { + error_instance.put( + global_this, + b"url", + ZigString::from_bytes(url) + .with_encoding() + .to_js(global_this), + ); + } + error_instance.put( + global_this, + b"name", + ZigString::from_bytes(name) + .with_encoding() + .to_js(global_this), + ); + error_instance.put( + global_this, + b"pkg", + ZigString::from_bytes(pkg) + .with_encoding() + .to_js(global_this), + ); + error_instance + } + + fn put_referrer(global_this: &JSGlobalObject, error_instance: JSValue, referrer: &[u8]) { + if !referrer.is_empty() && referrer != b"undefined" { + error_instance.put( + global_this, + b"referrer", + ZigString::from_bytes(referrer) + .with_encoding() + .to_js(global_this), + ); + } + } + + /// Sets `sourceURL`/`line`/`lineText`/`column` from the import record's + /// source location. + fn put_import_location( + &self, + global_this: &JSGlobalObject, + error_instance: JSValue, + import_record_id: u32, + ) { + let location = bun_ast::range_data( + Some(&self.parse_result.source), + self.parse_result.ast.import_records[import_record_id as usize].range, + b"", + ) + .location + .unwrap(); + error_instance.put( + global_this, + b"sourceURL", + ZigString::from_bytes(self.parse_result.source.path.text) + .with_encoding() + .to_js(global_this), + ); + error_instance.put( + global_this, + b"line", + JSValue::js_number(location.line as f64), + ); + if let Some(line_text) = location.line_text.as_deref() { + error_instance.put( + global_this, + b"lineText", + ZigString::from_bytes(line_text) + .with_encoding() + .to_js(global_this), + ); + } + error_instance.put( + global_this, + b"column", + JSValue::js_number(location.column as f64), + ); + } + + /// Rejects the module's promise with `error_instance` and drops the event + /// loop keepalive. The caller (`Queue::retain_mut`) returns `false` and + /// Vec drops the element, running Drop. + fn reject_with(&mut self, global_this: &JSGlobalObject, error_instance: JSValue) { + let promise_value = self.promise.swap(); + let promise = promise_value.as_internal_promise().unwrap(); + promise_value.ensure_still_alive(); + self.poll_ref.unref(bun_io::posix_event_loop::get_vm_ctx( + bun_io::AllocatorType::Js, + )); + // `JSInternalPromise` is an `opaque_ffi!` ZST handle; `opaque_mut` is + // the centralised non-null deref proof. + let _ = + JSInternalPromise::opaque_mut(promise).reject_as_handled(global_this, error_instance); + } + // write! into Vec // is infallible here; `.ok()` collapses the `fmt::Result`, so this never // actually returns Err — the wide Result is kept for call-site uniformity. @@ -870,32 +980,8 @@ impl AsyncModule { b"PackageResolveError" }; - let error_instance = ZigString::from_bytes(&msg) - .with_encoding() - .to_error_instance(global_this); - if !result.url.is_empty() { - error_instance.put( - global_this, - b"url", - ZigString::from_bytes(result.url) - .with_encoding() - .to_js(global_this), - ); - } - error_instance.put( - global_this, - b"name", - ZigString::from_bytes(name) - .with_encoding() - .to_js(global_this), - ); - error_instance.put( - global_this, - b"pkg", - ZigString::from_bytes(result.name) - .with_encoding() - .to_js(global_this), - ); + let error_instance = + Self::package_error_instance(global_this, &msg, name, result.url, result.name); error_instance.put( global_this, b"specifier", @@ -903,63 +989,11 @@ impl AsyncModule { .with_encoding() .to_js(global_this), ); - let location = bun_ast::range_data( - Some(&self.parse_result.source), - self.parse_result.ast.import_records[import_record_id as usize].range, - b"", - ) - .location - .unwrap(); - error_instance.put( - global_this, - b"sourceURL", - ZigString::from_bytes(self.parse_result.source.path.text) - .with_encoding() - .to_js(global_this), - ); - error_instance.put( - global_this, - b"line", - JSValue::js_number(location.line as f64), - ); - if let Some(line_text) = location.line_text.as_deref() { - error_instance.put( - global_this, - b"lineText", - ZigString::from_bytes(line_text) - .with_encoding() - .to_js(global_this), - ); - } - error_instance.put( - global_this, - b"column", - JSValue::js_number(location.column as f64), - ); - let referrer = self.referrer(); - if !referrer.is_empty() && referrer != b"undefined" { - error_instance.put( - global_this, - b"referrer", - ZigString::from_bytes(referrer) - .with_encoding() - .to_js(global_this), - ); - } + self.put_import_location(global_this, error_instance, import_record_id); + Self::put_referrer(global_this, error_instance, self.referrer()); - let promise_value = self.promise.swap(); - let promise = promise_value.as_internal_promise().unwrap(); - promise_value.ensure_still_alive(); let _ = vm; - self.poll_ref.unref(bun_io::posix_event_loop::get_vm_ctx( - bun_io::AllocatorType::Js, - )); - // The caller (Queue::retain_mut) returns `false` and Vec drops the - // element, running Drop. - // `JSInternalPromise` is an `opaque_ffi!` ZST handle; `opaque_mut` is - // the centralised non-null deref proof. - let _ = - JSInternalPromise::opaque_mut(promise).reject_as_handled(global_this, error_instance); + self.reject_with(global_this, error_instance); Ok(()) } @@ -1085,50 +1119,12 @@ impl AsyncModule { b"TarballDownloadError" }; - let error_instance = ZigString::from_bytes(&msg) - .with_encoding() - .to_error_instance(global_this); - if !result.url.is_empty() { - error_instance.put( - global_this, - b"url", - ZigString::from_bytes(result.url) - .with_encoding() - .to_js(global_this), - ); - } - error_instance.put( - global_this, - b"name", - ZigString::from_bytes(name) - .with_encoding() - .to_js(global_this), - ); - error_instance.put( - global_this, - b"pkg", - ZigString::from_bytes(result.name) - .with_encoding() - .to_js(global_this), - ); - let specifier = self.specifier(); - if !specifier.is_empty() && specifier != b"undefined" { - error_instance.put( - global_this, - b"referrer", - ZigString::from_bytes(specifier) - .with_encoding() - .to_js(global_this), - ); - } - - let location = bun_ast::range_data( - Some(&self.parse_result.source), - self.parse_result.ast.import_records[import_record_id as usize].range, - b"", - ) - .location - .unwrap(); + let error_instance = + Self::package_error_instance(global_this, &msg, name, result.url, result.name); + Self::put_referrer(global_this, error_instance, self.specifier()); + // `sourceURL` et al. follow `specifier` here (the resolve-error path + // puts `specifier` first), so the helper runs after this put; the + // location computation itself is pure. error_instance.put( global_this, b"specifier", @@ -1140,45 +1136,10 @@ impl AsyncModule { .with_encoding() .to_js(global_this), ); - error_instance.put( - global_this, - b"sourceURL", - ZigString::from_bytes(self.parse_result.source.path.text) - .with_encoding() - .to_js(global_this), - ); - error_instance.put( - global_this, - b"line", - JSValue::js_number(location.line as f64), - ); - if let Some(line_text) = location.line_text.as_deref() { - error_instance.put( - global_this, - b"lineText", - ZigString::from_bytes(line_text) - .with_encoding() - .to_js(global_this), - ); - } - error_instance.put( - global_this, - b"column", - JSValue::js_number(location.column as f64), - ); + self.put_import_location(global_this, error_instance, import_record_id); - let promise_value = self.promise.swap(); - let promise = promise_value.as_internal_promise().unwrap(); - promise_value.ensure_still_alive(); let _ = vm; - self.poll_ref.unref(bun_io::posix_event_loop::get_vm_ctx( - bun_io::AllocatorType::Js, - )); - // Caller drops via retain_mut → false. - // `JSInternalPromise` is an `opaque_ffi!` ZST handle; `opaque_mut` is - // the centralised non-null deref proof. - let _ = - JSInternalPromise::opaque_mut(promise).reject_as_handled(global_this, error_instance); + self.reject_with(global_this, error_instance); Ok(()) } diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index 96455a9a6513..6ae32f80169e 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -701,10 +701,8 @@ impl<'a> TablePrinter<'a> { let mut value_formatter = self.value_formatter.shallow_clone(); let tag = formatter::Tag::get(value, self.global_object)?; - value_formatter.quote_strings = !(matches!( - tag.tag, - TagPayload::String | TagPayload::StringPossiblyFormatted - )); + value_formatter.quote_strings = + !(matches!(tag.tag, Tag::String | Tag::StringPossiblyFormatted)); let _ = value_formatter.format::(tag, &mut counter, value, self.global_object); // VisibleCharacterCounter write cannot fail. let _ = bun_io::Write::flush(&mut counter); @@ -882,10 +880,8 @@ impl<'a> TablePrinter<'a> { let tag = formatter::Tag::get(value, self.global_object)?; let mut value_formatter = self.value_formatter.shallow_clone(); - value_formatter.quote_strings = !(matches!( - tag.tag, - TagPayload::String | TagPayload::StringPossiblyFormatted - )); + value_formatter.quote_strings = + !(matches!(tag.tag, Tag::String | Tag::StringPossiblyFormatted)); // Release pooled visit map after formatting. // `shallow_clone()` guarantees the source's `map_node` is @@ -1485,7 +1481,7 @@ pub fn format2( return Ok(()); } - if matches!(tag.tag, TagPayload::String) { + if matches!(tag.tag, Tag::String) { if options.enable_colors { if level == MessageLevel::Error { let _ = writer.write_all(pfmt!("", true).as_bytes()); @@ -1561,8 +1557,8 @@ pub fn format2( any = true; tag = formatter::Tag::get(this_value, global)?; - if matches!(tag.tag, TagPayload::String) && !fmt.remaining().is_empty() { - tag.tag = TagPayload::StringPossiblyFormatted; + if matches!(tag.tag, Tag::String) && !fmt.remaining().is_empty() { + tag.tag = Tag::StringPossiblyFormatted; } fmt.format::(tag, writer, this_value, global)?; @@ -1583,8 +1579,8 @@ pub fn format2( } any = true; tag = formatter::Tag::get(this_value, global)?; - if matches!(tag.tag, TagPayload::String) && !fmt.remaining().is_empty() { - tag.tag = TagPayload::StringPossiblyFormatted; + if matches!(tag.tag, Tag::String) && !fmt.remaining().is_empty() { + tag.tag = Tag::StringPossiblyFormatted; } fmt.format::(tag, writer, this_value, global)?; @@ -1613,7 +1609,7 @@ pub struct CustomFormattedObject { // Formatter // ─────────────────────────────────────────────────────────────────────────── -pub use formatter::{Formatter, Tag, TagOptions, TagPayload, TagResult, visited}; +pub use formatter::{Formatter, Tag, TagOptions, TagResult, visited}; pub mod formatter { use super::*; @@ -2055,156 +2051,20 @@ pub mod formatter { } } - /// Only `CustomFormattedObject` carries a payload. - #[derive(Copy, Clone, PartialEq, Eq)] - pub enum TagPayload { - StringPossiblyFormatted, - String, - Undefined, - Double, - Integer, - Null, - Boolean, - Array, - Object, - Function, - Class, - Error, - TypedArray, - Map, - MapIterator, - SetIterator, - Set, - BigInt, - Symbol, - CustomFormattedObject(CustomFormattedObject), - GlobalObject, - Private, - Promise, - JSON, - ToJSON, - NativeCode, - JSX, - Event, - GetterSetter, - CustomGetterSetter, - Proxy, - RevokedProxy, - } - - impl TagPayload { - /// The constructor lives here as well as on the bare - /// discriminant `Tag`. Callers in sibling modules use either name. - #[inline] - pub fn get(value: JSValue, global_this: &JSGlobalObject) -> JsResult { - Tag::get(value, global_this) - } - /// Delegates to `Tag::get_advanced`. - #[inline] - pub fn get_advanced( - value: JSValue, - global_this: &JSGlobalObject, - opts: TagOptions, - ) -> JsResult { - Tag::get_advanced(value, global_this, opts) - } - pub fn is_primitive(self) -> bool { - self.tag().is_primitive() - } - pub fn tag(self) -> Tag { - match self { - TagPayload::StringPossiblyFormatted => Tag::StringPossiblyFormatted, - TagPayload::String => Tag::String, - TagPayload::Undefined => Tag::Undefined, - TagPayload::Double => Tag::Double, - TagPayload::Integer => Tag::Integer, - TagPayload::Null => Tag::Null, - TagPayload::Boolean => Tag::Boolean, - TagPayload::Array => Tag::Array, - TagPayload::Object => Tag::Object, - TagPayload::Function => Tag::Function, - TagPayload::Class => Tag::Class, - TagPayload::Error => Tag::Error, - TagPayload::TypedArray => Tag::TypedArray, - TagPayload::Map => Tag::Map, - TagPayload::MapIterator => Tag::MapIterator, - TagPayload::SetIterator => Tag::SetIterator, - TagPayload::Set => Tag::Set, - TagPayload::BigInt => Tag::BigInt, - TagPayload::Symbol => Tag::Symbol, - TagPayload::CustomFormattedObject(_) => Tag::CustomFormattedObject, - TagPayload::GlobalObject => Tag::GlobalObject, - TagPayload::Private => Tag::Private, - TagPayload::Promise => Tag::Promise, - TagPayload::JSON => Tag::JSON, - TagPayload::ToJSON => Tag::ToJSON, - TagPayload::NativeCode => Tag::NativeCode, - TagPayload::JSX => Tag::JSX, - TagPayload::Event => Tag::Event, - TagPayload::GetterSetter => Tag::GetterSetter, - TagPayload::CustomGetterSetter => Tag::CustomGetterSetter, - TagPayload::Proxy => Tag::Proxy, - TagPayload::RevokedProxy => Tag::RevokedProxy, - } - } - } - - /// Reverse of [`TagPayload::tag`]. The `CustomFormattedObject` arm gets a - /// default (zero) payload — used by the `ConsoleFormatter` trait bridge in - /// `lib.rs`, which never passes that tag (write_format hooks pick concrete - /// tags like `Double` / `Boolean` / `Object` / `Private`). - impl From for TagPayload { - fn from(t: Tag) -> Self { - match t { - Tag::StringPossiblyFormatted => TagPayload::StringPossiblyFormatted, - Tag::String => TagPayload::String, - Tag::Undefined => TagPayload::Undefined, - Tag::Double => TagPayload::Double, - Tag::Integer => TagPayload::Integer, - Tag::Null => TagPayload::Null, - Tag::Boolean => TagPayload::Boolean, - Tag::Array => TagPayload::Array, - Tag::Object => TagPayload::Object, - Tag::Function => TagPayload::Function, - Tag::Class => TagPayload::Class, - Tag::Error => TagPayload::Error, - Tag::TypedArray => TagPayload::TypedArray, - Tag::Map => TagPayload::Map, - Tag::MapIterator => TagPayload::MapIterator, - Tag::SetIterator => TagPayload::SetIterator, - Tag::Set => TagPayload::Set, - Tag::BigInt => TagPayload::BigInt, - Tag::Symbol => TagPayload::Symbol, - Tag::CustomFormattedObject => { - TagPayload::CustomFormattedObject(CustomFormattedObject::default()) - } - Tag::GlobalObject => TagPayload::GlobalObject, - Tag::Private => TagPayload::Private, - Tag::Promise => TagPayload::Promise, - Tag::JSON => TagPayload::JSON, - Tag::ToJSON => TagPayload::ToJSON, - Tag::NativeCode => TagPayload::NativeCode, - Tag::JSX => TagPayload::JSX, - Tag::Event => TagPayload::Event, - Tag::GetterSetter => TagPayload::GetterSetter, - Tag::CustomGetterSetter => TagPayload::CustomGetterSetter, - Tag::Proxy => TagPayload::Proxy, - Tag::RevokedProxy => TagPayload::RevokedProxy, - } - } - } - #[derive(Copy, Clone)] pub struct TagResult { - pub tag: TagPayload, + pub tag: Tag, pub cell: jsc::JSType, + /// Set only when `tag` is [`Tag::CustomFormattedObject`]. + pub custom: Option, } impl Default for TagResult { fn default() -> Self { Self { - tag: TagPayload::Undefined, + tag: Tag::Undefined, cell: jsc::JSType::Cell, + custom: None, } } } @@ -2231,37 +2091,37 @@ pub mod formatter { ) -> JsResult { if value.is_empty() || value == JSValue::UNDEFINED { return Ok(TagResult { - tag: TagPayload::Undefined, + tag: Tag::Undefined, ..Default::default() }); } if value == JSValue::NULL { return Ok(TagResult { - tag: TagPayload::Null, + tag: Tag::Null, ..Default::default() }); } if value.is_int32() { return Ok(TagResult { - tag: TagPayload::Integer, + tag: Tag::Integer, ..Default::default() }); } else if value.is_number() { return Ok(TagResult { - tag: TagPayload::Double, + tag: Tag::Double, ..Default::default() }); } else if value.is_boolean() { return Ok(TagResult { - tag: TagPayload::Boolean, + tag: Tag::Boolean, ..Default::default() }); } if !value.is_cell() { return Ok(TagResult { - tag: TagPayload::NativeCode, + tag: Tag::NativeCode, ..Default::default() }); } @@ -2270,15 +2130,17 @@ pub mod formatter { if js_type.is_hidden() { return Ok(TagResult { - tag: TagPayload::NativeCode, + tag: Tag::NativeCode, cell: js_type, + custom: None, }); } if js_type == jsc::JSType::Cell { return Ok(TagResult { - tag: TagPayload::NativeCode, + tag: Tag::NativeCode, cell: js_type, + custom: None, }); } @@ -2290,17 +2152,18 @@ pub mod formatter { match value.fast_get(global_this, jsc::BuiltinName::InspectCustom) { Err(_) => { return Ok(TagResult { - tag: TagPayload::RevokedProxy, + tag: Tag::RevokedProxy, ..Default::default() }); } Ok(Some(callback_value)) if callback_value.is_callable() => { return Ok(TagResult { - tag: TagPayload::CustomFormattedObject(CustomFormattedObject { + tag: Tag::CustomFormattedObject, + cell: js_type, + custom: Some(CustomFormattedObject { function: callback_value, this: value, }), - cell: js_type, }); } _ => {} @@ -2309,8 +2172,9 @@ pub mod formatter { if js_type == jsc::JSType::DOMWrapper { return Ok(TagResult { - tag: TagPayload::Private, + tag: Tag::Private, cell: js_type, + custom: None, }); } @@ -2321,8 +2185,9 @@ pub mod formatter { { if value.is_class(global_this) { return Ok(TagResult { - tag: TagPayload::Class, + tag: Tag::Class, cell: js_type, + custom: None, }); } @@ -2334,11 +2199,12 @@ pub mod formatter { // handle the prefix in the .Object formatter. return Ok(TagResult { tag: if js_type == jsc::JSType::InternalFunction { - TagPayload::Object + Tag::Object } else { - TagPayload::Function + Tag::Function }, cell: js_type, + custom: None, }); } @@ -2353,8 +2219,9 @@ pub mod formatter { return Tag::get(target, global_this); } return Ok(TagResult { - tag: TagPayload::GlobalObject, + tag: Tag::GlobalObject, cell: js_type, + custom: None, }); } @@ -2379,8 +2246,9 @@ pub mod formatter { global_this, )? { return Ok(TagResult { - tag: TagPayload::JSX, + tag: Tag::JSX, cell: js_type, + custom: None, }); } } @@ -2388,24 +2256,24 @@ pub mod formatter { use jsc::JSType as T; let tag = match js_type { - T::ErrorInstance => TagPayload::Error, - T::NumberObject => TagPayload::Double, + T::ErrorInstance => Tag::Error, + T::NumberObject => Tag::Double, T::DerivedArray | T::Array | T::DirectArguments | T::ScopedArguments - | T::ClonedArguments => TagPayload::Array, - T::DerivedStringObject | T::String | T::StringObject => TagPayload::String, - T::RegExpObject => TagPayload::String, - T::Symbol => TagPayload::Symbol, - T::BooleanObject => TagPayload::Boolean, - T::JSFunction => TagPayload::Function, - T::WeakMap | T::Map => TagPayload::Map, - T::MapIterator => TagPayload::MapIterator, - T::SetIterator => TagPayload::SetIterator, - T::WeakSet | T::Set => TagPayload::Set, - T::JSDate => TagPayload::JSON, - T::JSPromise => TagPayload::Promise, + | T::ClonedArguments => Tag::Array, + T::DerivedStringObject | T::String | T::StringObject => Tag::String, + T::RegExpObject => Tag::String, + T::Symbol => Tag::Symbol, + T::BooleanObject => Tag::Boolean, + T::JSFunction => Tag::Function, + T::WeakMap | T::Map => Tag::Map, + T::MapIterator => Tag::MapIterator, + T::SetIterator => Tag::SetIterator, + T::WeakSet | T::Set => Tag::Set, + T::JSDate => Tag::JSON, + T::JSPromise => Tag::Promise, T::WrapForValidIterator | T::RegExpStringIterator @@ -2414,43 +2282,31 @@ pub mod formatter { | T::IteratorHelper | T::Object | T::FinalObject - | T::ModuleNamespaceObject => TagPayload::Object, + | T::ModuleNamespaceObject => Tag::Object, T::ProxyObject => { let handler = value.get_proxy_internal_field(jsc::ProxyField::Handler); if handler.is_empty() || handler.is_undefined_or_null() { return Ok(TagResult { - tag: TagPayload::RevokedProxy, + tag: Tag::RevokedProxy, cell: js_type, + custom: None, }); } - TagPayload::Proxy + Tag::Proxy } T::GlobalObject => { if !opts.contains(TagOptions::HIDE_GLOBAL) { - TagPayload::Object + Tag::Object } else { - TagPayload::GlobalObject + Tag::GlobalObject } } - T::ArrayBuffer - | T::Int8Array - | T::Uint8Array - | T::Uint8ClampedArray - | T::Int16Array - | T::Uint16Array - | T::Int32Array - | T::Uint32Array - | T::Float16Array - | T::Float32Array - | T::Float64Array - | T::BigInt64Array - | T::BigUint64Array - | T::DataView => TagPayload::TypedArray, - - T::HeapBigInt => TagPayload::BigInt, + t if t.is_array_buffer_like() => Tag::TypedArray, + + T::HeapBigInt => Tag::BigInt, // None of these should ever exist here // But we're going to check anyway @@ -2473,18 +2329,22 @@ pub mod formatter { | T::LexicalEnvironment | T::ModuleEnvironment | T::StrictEvalActivation - | T::WithScope => TagPayload::NativeCode, + | T::WithScope => Tag::NativeCode, - T::Event => TagPayload::Event, + T::Event => Tag::Event, - T::GetterSetter => TagPayload::GetterSetter, - T::CustomGetterSetter => TagPayload::CustomGetterSetter, + T::GetterSetter => Tag::GetterSetter, + T::CustomGetterSetter => Tag::CustomGetterSetter, - T::JSAsJSONType => TagPayload::ToJSON, + T::JSAsJSONType => Tag::ToJSON, - _ => TagPayload::JSON, + _ => Tag::JSON, }; - Ok(TagResult { tag, cell: js_type }) + Ok(TagResult { + tag, + cell: js_type, + custom: None, + }) } } @@ -2507,11 +2367,7 @@ pub mod formatter { slice_: &[u8], global: &'a JSGlobalObject, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let mut slice = slice_; let mut i: u32 = 0; let mut len: u32 = slice.len() as u32; @@ -2588,11 +2444,8 @@ pub mod formatter { next_value, next_value.js_type(), )?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = + WrappedWriter::new(writer_, &mut self.estimated_line_length); } PercentTag::I => { // 1. If Type(current) is Symbol, let converted be NaN @@ -2740,11 +2593,8 @@ pub mod formatter { next_value, global, )?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = + WrappedWriter::new(writer_, &mut self.estimated_line_length); } PercentTag::C => { @@ -2789,6 +2639,14 @@ pub mod formatter { impl<'w> WrappedWriter<'w> { pub const IS_WRAPPED_WRITER: bool = true; + pub fn new(ctx: &'w mut dyn bun_io::Write, estimated_line_length: &'w mut usize) -> Self { + Self { + ctx, + failed: false, + estimated_line_length, + } + } + /// Mirror of `Formatter::add_for_new_line` routed through the borrowed /// `estimated_line_length` so callers don't need a second `&mut self` /// on the parent `Formatter` while a `WrappedWriter` is live. @@ -3141,11 +2999,8 @@ pub mod formatter { value: JSValue, ) -> JsResult<()> { if value.is_cell() && !value.js_type().is_function() { - let mut writer = WrappedWriter { - ctx: self.writer, - failed: false, - estimated_line_length: &mut self.formatter.estimated_line_length, - }; + let mut writer = + WrappedWriter::new(self.writer, &mut self.formatter.estimated_line_length); if let Some(name_str) = get_object_name(global_this, value)? { writer.print(format_args!("{name_str} ")); @@ -3301,11 +3156,8 @@ pub mod formatter { } } - let mut writer = WrappedWriter { - ctx: &mut *ctx.writer, - failed: false, - estimated_line_length: &mut ctx.formatter.estimated_line_length, - }; + let mut writer = + WrappedWriter::new(&mut *ctx.writer, &mut ctx.formatter.estimated_line_length); if ctx.i > 0 { writer.print_comma::(); } @@ -3592,11 +3444,7 @@ pub mod formatter { &mut self, writer_: &mut dyn bun_io::Write, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); writer.add_for_new_line(9); writer.print(format_args!( "{}undefined{}", @@ -3611,11 +3459,7 @@ pub mod formatter { #[inline(never)] fn print_null(&mut self, writer_: &mut dyn bun_io::Write) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); writer.add_for_new_line(4); writer.print(format_args!( "{}null{}", @@ -3634,11 +3478,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if let Some(class_name) = value.get_class_info_name() { writer.add_for_new_line("[native code: ]".len() + class_name.len()); writer.write_all(b"[native code: "); @@ -3659,11 +3499,7 @@ pub mod formatter { &mut self, writer_: &mut dyn bun_io::Write, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); const FMT: &str = "[Global Object]"; writer.add_for_new_line(FMT.len()); writer.write_all(pfmt!(concat!("", "[Global Object]", ""), C).as_bytes()); @@ -3678,11 +3514,7 @@ pub mod formatter { &mut self, writer_: &mut dyn bun_io::Write, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); writer.add_for_new_line("".len()); writer.print(format_args!( "{}{}", @@ -3737,11 +3569,7 @@ pub mod formatter { // This is called from the '%s' formatter, so it can actually be any value use crate::StringJsc as _; let str = OwnedString::new(BunString::from_js(value, self.global_this)?); - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); writer.add_for_new_line(str.length()); if self.quote_strings && js_type != jsc::JSType::RegExpObject { @@ -3791,11 +3619,7 @@ pub mod formatter { self.failed = true; } self.print_as::(Tag::JSON, writer_, value, jsc::JSType::StringObject)?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); } else { JSPrinter::write_json_string( str.latin1(), @@ -3849,11 +3673,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let int = value.coerce_to_int64(self.global_this)?; writer.add_for_new_line(bun_core::fmt::digit_count(int)); writer.print(format_args!( @@ -3874,11 +3694,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let zstr = value.get_zig_string(self.global_this)?; let out_str = zstr.slice(); writer.add_for_new_line(out_str.len()); @@ -3900,16 +3716,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if value.is_cell() { let mut number_name = ZigString::EMPTY; value.get_class_name(self.global_this, &mut number_name)?; @@ -3923,10 +3730,10 @@ pub mod formatter { ); writer.print(format_args!( "{}[Number ({}): {}]{}", - pf!(""), + pfmt!("", C), number_name, number_value, - pf!("") + pfmt!("", C) )); if writer.failed { self.failed = true; @@ -3937,10 +3744,10 @@ pub mod formatter { writer.add_for_new_line(number_name.len + number_value.len + 4); writer.print(format_args!( "{}[{}: {}]{}", - pf!(""), + pfmt!("", C), number_name, number_value, - pf!("") + pfmt!("", C) )); if writer.failed { self.failed = true; @@ -3952,26 +3759,34 @@ pub mod formatter { if num.is_infinite() && num > 0.0 { writer.add_for_new_line("Infinity".len()); - writer.print(format_args!("{}Infinity{}", pf!(""), pf!(""))); + writer.print(format_args!( + "{}Infinity{}", + pfmt!("", C), + pfmt!("", C) + )); } else if num.is_infinite() && num < 0.0 { writer.add_for_new_line("-Infinity".len()); writer.print(format_args!( "{}-Infinity{}", - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C) )); } else if num.is_nan() { writer.add_for_new_line("NaN".len()); - writer.print(format_args!("{}NaN{}", pf!(""), pf!(""))); + writer.print(format_args!( + "{}NaN{}", + pfmt!("", C), + pfmt!("", C) + )); } else { let mut buf = [0u8; 124]; let formatted = bun_core::fmt::FormatDouble::dtoa_with_negative_zero(&mut buf, num); writer.add_for_new_line(formatted.len()); writer.print(format_args!( "{}{}{}", - pf!(""), + pfmt!("", C), bstr::BStr::new(formatted), - pf!("") + pfmt!("", C) )); } if writer.failed { @@ -4024,11 +3839,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let description = value.get_description(self.global_this); writer.add_for_new_line("Symbol".len()); @@ -4091,16 +3902,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); // Prefer the constructor's own `.name` property over // `getClassName` / `calculatedClassName`. For DOM / WebCore // InternalFunction constructors like `ReadableStreamBYOBReader`, @@ -4130,31 +3932,31 @@ pub mod formatter { if printable_proto.is_empty() { writer.print(format_args!( "{}[class (anonymous)]{}", - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C) )); } else { writer.print(format_args!( "{}[class (anonymous) extends {}]{}", - pf!(""), + pfmt!("", C), printable_proto, - pf!("") + pfmt!("", C) )); } } else if printable_proto.is_empty() { writer.print(format_args!( "{}[class {}]{}", - pf!(""), + pfmt!("", C), printable, - pf!("") + pfmt!("", C) )); } else { writer.print(format_args!( "{}[class {} extends {}]{}", - pf!(""), + pfmt!("", C), printable, printable_proto, - pf!("") + pfmt!("", C) )); } if writer.failed { @@ -4169,16 +3971,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let printable = OwnedString::new(value.get_name(self.global_this)?); let proto = value.get_prototype(self.global_this); @@ -4187,29 +3980,33 @@ pub mod formatter { if printable.is_empty() || func_name.eql(&printable) { if func_name.is_empty() { - writer.print(format_args!("{}[Function]{}", pf!(""), pf!(""))); + writer.print(format_args!( + "{}[Function]{}", + pfmt!("", C), + pfmt!("", C) + )); } else { writer.print(format_args!( "{}[{}]{}", - pf!(""), + pfmt!("", C), func_name, - pf!("") + pfmt!("", C) )); } } else if func_name.is_empty() { writer.print(format_args!( "{}[Function: {}]{}", - pf!(""), + pfmt!("", C), printable, - pf!("") + pfmt!("", C) )); } else { writer.print(format_args!( "{}[{}: {}]{}", - pf!(""), + pfmt!("", C), func_name, printable, - pf!("") + pfmt!("", C) )); } if writer.failed { @@ -4224,11 +4021,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); // `JSCell` is an `opaque_ffi!` ZST handle; `opaque_ref` is the // centralised non-null deref proof (tag only produced for cells). let cell = jsc::JSCell::opaque_ref(value.to_cell().expect("GetterSetter is a cell")); @@ -4270,11 +4063,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if !self.single_line && writer.good_time_for_a_new_line(self.indent) { writer.write_all(b"\n"); writer.write_indent(self.indent); @@ -4307,16 +4096,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if value.is_cell() { let mut bool_name = ZigString::EMPTY; value.get_class_name(self.global_this, &mut bool_name)?; @@ -4328,10 +4108,10 @@ pub mod formatter { .add_for_new_line(bool_value.len + bool_name.len + "[Boolean (): ]".len()); writer.print(format_args!( "{}[Boolean ({}): {}]{}", - pf!(""), + pfmt!("", C), bool_name, bool_value, - pf!("") + pfmt!("", C) )); if writer.failed { self.failed = true; @@ -4341,9 +4121,9 @@ pub mod formatter { writer.add_for_new_line(bool_value.len + "[Boolean: ]".len()); writer.print(format_args!( "{}[Boolean: {}]{}", - pf!(""), + pfmt!("", C), bool_value, - pf!("") + pfmt!("", C) )); if writer.failed { self.failed = true; @@ -4352,10 +4132,10 @@ pub mod formatter { } if value.to_boolean() { writer.add_for_new_line(4); - writer.write_all(pf!("true").as_bytes()); + writer.write_all(pfmt!("true", C).as_bytes()); } else { writer.add_for_new_line(5); - writer.write_all(pf!("false").as_bytes()); + writer.write_all(pfmt!("false", C).as_bytes()); } if writer.failed { self.failed = true; @@ -4397,11 +4177,7 @@ pub mod formatter { value: JSValue, js_type: jsc::JSType, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let mut str = OwnedString::new(BunString::empty()); value.json_stringify(self.global_this, self.indent, &mut str)?; @@ -4455,16 +4231,7 @@ pub mod formatter { // function, and `WrappedWriter` holds `&mut self.estimated_line_length` // which prevents calling `&self` methods while it is live. let tag_opts = self.tag_opts(); - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let len = value.get_length(self.global_this)?; @@ -4518,11 +4285,7 @@ pub mod formatter { } self.format::(tag, writer_, element, self.global_this)?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if tag.cell.is_string_like() && C { writer.write_all(pfmt!("", true).as_bytes()); @@ -4550,9 +4313,9 @@ pub mod formatter { "... N more items".len(), format_args!( "{}... {} more items{}", - pf!(""), + pfmt!("", C), len - u64::from(i), - pf!("") + pfmt!("", C) ), ); break; @@ -4577,7 +4340,7 @@ pub mod formatter { if empty_count == 1 { writer.pretty::( "empty item".len(), - format_args!("{}empty item{}", pf!(""), pf!("")), + format_args!("{}empty item{}", pfmt!("", C), pfmt!("", C)), ); } else { writer.add_for_new_line(bun_core::fmt::digit_count(empty_count)); @@ -4585,9 +4348,9 @@ pub mod formatter { " x empty items".len(), format_args!( "{}{} x empty items{}", - pf!(""), + pfmt!("", C), empty_count, - pf!("") + pfmt!("", C) ), ); } @@ -4608,11 +4371,7 @@ pub mod formatter { let tag = Tag::get_advanced(element, self.global_this, tag_opts)?; self.format::(tag, writer_, element, self.global_this)?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if tag.cell.is_string_like() && C { writer.write_all(pfmt!("", true).as_bytes()); @@ -4639,7 +4398,7 @@ pub mod formatter { if empty_count == 1 { writer.pretty::( "empty item".len(), - format_args!("{}empty item{}", pf!(""), pf!("")), + format_args!("{}empty item{}", pfmt!("", C), pfmt!("", C)), ); } else { writer.add_for_new_line(bun_core::fmt::digit_count(empty_count)); @@ -4647,9 +4406,9 @@ pub mod formatter { " x empty items".len(), format_args!( "{}{} x empty items{}", - pf!(""), + pfmt!("", C), empty_count, - pf!("") + pfmt!("", C) ), ); } @@ -4679,11 +4438,7 @@ pub mod formatter { if self.failed { return Ok(()); } - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); } } @@ -5008,12 +4763,6 @@ pub mod formatter { value: JSValue, remove_before_recurse: &mut bool, ) -> JsResult<()> { - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } - let event_type_value: JSValue = 'brk: { let Some(value_) = value.get(self.global_this, "type")? else { break 'brk JSValue::UNDEFINED; @@ -5052,9 +4801,9 @@ pub mod formatter { let _ = writeln!( writer_, "{}{}{} {{", - pf!(""), + pfmt!("", C), event_tag_name, - pf!("") + pfmt!("", C) ); { self.indent += 1; @@ -5070,23 +4819,23 @@ pub mod formatter { let _ = write!( writer_, "{}type: {}\"{}\"{}{},{} ", - pf!(""), - pf!(""), + pfmt!("", C), + pfmt!("", C), bstr::BStr::new(event_type.label()), - pf!(""), - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C), + pfmt!("", C) ); } else { let _ = writeln!( writer_, "{}type: {}\"{}\"{}{},{}", - pf!(""), - pf!(""), + pfmt!("", C), + pfmt!("", C), bstr::BStr::new(event_type.label()), - pf!(""), - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C), + pfmt!("", C) ); } @@ -5100,9 +4849,9 @@ pub mod formatter { let _ = write!( writer_, "{}message{}:{} ", - pf!(""), - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C), + pfmt!("", C) ); let tag = Tag::get_advanced(message_value, self.global_this, self.tag_opts())?; @@ -5125,9 +4874,9 @@ pub mod formatter { let _ = write!( writer_, "{}data{}:{} ", - pf!(""), - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C), + pfmt!("", C) ); let data: JSValue = value .fast_get(self.global_this, jsc::BuiltinName::Data)? @@ -5152,9 +4901,9 @@ pub mod formatter { let _ = write!( writer_, "{}error{}:{} ", - pf!(""), - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C), + pfmt!("", C) ); let tag = Tag::get_advanced(error_value, self.global_this, self.tag_opts())?; @@ -5188,22 +4937,13 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } // Cache once: `disable_inspect_custom` does not change inside this // function, and `WrappedWriter` holds `&mut self.estimated_line_length` // which prevents calling `&self` methods while it is live. let tag_opts = self.tag_opts(); - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); - writer.write_all(pf!("").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); writer.write_all(b"<"); // Both arms of the `type` if/else below assign these, so deferred @@ -5241,13 +4981,13 @@ pub mod formatter { } if !is_tag_kind_primitive { - writer.write_all(pf!("").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); } else { - writer.write_all(pf!("").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); } writer.write_all(tag_name_slice.slice()); if C { - writer.write_all(pf!("").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); } if let Some(key_value) = value.get(self.global_this, "key")? { @@ -5271,11 +5011,7 @@ pub mod formatter { key_value, self.global_this, )?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); needs_space = true; } @@ -5331,10 +5067,10 @@ pub mod formatter { writer.print(format_args!( "{}{}{}={}", - pf!(""), + pfmt!("", C), prop.trunc(128), - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C) )); if tag.cell.is_string_like() && C { @@ -5345,11 +5081,7 @@ pub mod formatter { self.failed = true; } self.format::(tag, writer_, property_value, self.global_this)?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if tag.cell.is_string_like() && C { writer.write_all(pfmt!("", true).as_bytes()); @@ -5380,12 +5112,11 @@ pub mod formatter { if let Some(children) = children_prop { let tag = Tag::get(children, self.global_this)?; - let print_children = - matches!(tag.tag.tag(), Tag::String | Tag::JSX | Tag::Array); + let print_children = matches!(tag.tag, Tag::String | Tag::JSX | Tag::Array); if print_children && !self.single_line { 'print_children: { - match tag.tag.tag() { + match tag.tag { Tag::String => { let children_string = children.get_zig_string(self.global_this)?; @@ -5426,12 +5157,10 @@ pub mod formatter { children, self.global_this, )?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self - .estimated_line_length, - }; + writer = WrappedWriter::new( + writer_, + &mut self.estimated_line_length, + ); } writer.write_all(b"\n"); write_indent_n(self.indent, writer.ctx) @@ -5474,12 +5203,10 @@ pub mod formatter { child, self.global_this, )?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self - .estimated_line_length, - }; + writer = WrappedWriter::new( + writer_, + &mut self.estimated_line_length, + ); if (j as u64) + 1 < length { writer.write_all(b"\n"); write_indent_n(self.indent, writer.ctx) @@ -5497,13 +5224,13 @@ pub mod formatter { writer.write_all(b"").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); } else { - writer.write_all(pf!("").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); } writer.write_all(tag_name_slice.slice()); if C { - writer.write_all(pf!("").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); } writer.write_all(b">"); } @@ -5611,11 +5338,6 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } if self.single_line { let _ = writer_.write_all(b" "); } else if self.always_newline_scope || self.good_time_for_a_new_line() { @@ -5631,9 +5353,9 @@ pub mod formatter { let _ = write!( writer_, "{}[{} ...]{}", - pf!(""), + pfmt!("", C), display_name, - pf!("") + pfmt!("", C) ); Ok(()) } @@ -5683,11 +5405,7 @@ pub mod formatter { value: JSValue, js_type: jsc::JSType, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let array_buffer = value.as_array_buffer(self.global_this).unwrap(); let slice = array_buffer.byte_slice(); @@ -5826,10 +5544,10 @@ pub mod formatter { let _restore = defer_restore!(self.global_this, prev_global_this); self.global_this = global_this; - if let TagPayload::CustomFormattedObject(obj) = result.tag { + if let Some(obj) = result.custom { self.custom_formatted_object = obj; } - self.print_as::(result.tag.tag(), writer, value, result.cell) + self.print_as::(result.tag, writer, value, result.cell) } } diff --git a/src/jsc/URL.rs b/src/jsc/URL.rs index bc2fe0fa2cb2..67fa8981ed48 100644 --- a/src/jsc/URL.rs +++ b/src/jsc/URL.rs @@ -3,150 +3,33 @@ use core::ptr::NonNull; use bun_core::String; use bun_jsc::{JSGlobalObject, JSValue, JsResult}; -bun_opaque::opaque_ffi! { - /// Opaque handle to a WebKit `WTF::URL` allocated on the C++ side. - pub struct URL; -} +// The JSC-agnostic surface (constructors, getters, `destroy`, the +// whole-string conversions) lives in `bun_url::whatwg`; only the entry +// points that need `JSValue`/`JSGlobalObject` stay in this crate, as the +// `UrlJsc` extension trait. +pub use bun_url::whatwg::URL; -// Getters take `&URL` (non-null `*const URL` at the C ABI; BunString.cpp never -// mutates the WTF::URL on read). `&mut String` for the in/out params is -// ABI-identical to non-null `*mut String`. `URL__deinit` consumes the C++ -// allocation, so it keeps a raw pointer and stays `unsafe fn`. unsafe extern "C" { safe fn URL__fromJS(value: JSValue, global: &JSGlobalObject) -> *mut URL; - safe fn URL__fromString(input: &mut String) -> *mut URL; - safe fn URL__protocol(url: &URL) -> String; - safe fn URL__href(url: &URL) -> String; - safe fn URL__username(url: &URL) -> String; - safe fn URL__password(url: &URL) -> String; - safe fn URL__search(url: &URL) -> String; - safe fn URL__host(url: &URL) -> String; - safe fn URL__hostname(url: &URL) -> String; - safe fn URL__port(url: &URL) -> u32; - fn URL__deinit(url: *mut URL); - safe fn URL__pathname(url: &URL) -> String; safe fn URL__getHrefFromJS(value: JSValue, global: &JSGlobalObject) -> String; - safe fn URL__getHref(input: &mut String) -> String; - safe fn URL__getFileURLString(input: &mut String) -> String; - safe fn URL__getHrefJoin(base: &mut String, relative: &mut String) -> String; - safe fn URL__pathFromFileURL(input: &mut String) -> String; - safe fn URL__hash(url: &URL) -> String; - safe fn URL__fragmentIdentifier(url: &URL) -> String; } -impl URL { - /// Includes the leading '#'. - pub fn hash(&self) -> String { - URL__hash(self) - } - - /// Exactly the same as hash, excluding the leading '#'. - pub fn fragment_identifier(&self) -> String { - URL__fragmentIdentifier(self) - } - - pub fn href_from_string(str: String) -> String { - let mut input = str; - URL__getHref(&mut input) - } - - pub fn join(base: String, relative: String) -> String { - let mut base_str = base; - let mut relative_str = relative; - URL__getHrefJoin(&mut base_str, &mut relative_str) - } - - pub fn file_url_from_string(str: String) -> String { - let mut input = str; - URL__getFileURLString(&mut input) - } - - pub fn path_from_file_url(str: String) -> String { - let mut input = str; - URL__pathFromFileURL(&mut input) - } +pub trait UrlJsc: Sized { + /// This percent-encodes the URL, punycode-encodes the hostname, and returns the result. + /// If it fails, the tag is marked Dead. + fn href_from_js(value: JSValue, global: &JSGlobalObject) -> JsResult; + /// Returns an owned C++ heap pointer that the caller must `destroy()`. + fn from_js(value: JSValue, global: &JSGlobalObject) -> JsResult>>; +} - /// This percent-encodes the URL, punycode-encodes the hostname, and returns the result - /// If it fails, the tag is marked Dead +impl UrlJsc for URL { #[track_caller] - pub fn href_from_js(value: JSValue, global: &JSGlobalObject) -> JsResult { + fn href_from_js(value: JSValue, global: &JSGlobalObject) -> JsResult { crate::call_check_slow(global, || URL__getHrefFromJS(value, global)) } #[track_caller] - pub fn from_js(value: JSValue, global: &JSGlobalObject) -> JsResult>> { + fn from_js(value: JSValue, global: &JSGlobalObject) -> JsResult>> { crate::call_check_slow(global, || URL__fromJS(value, global)).map(NonNull::new) } - - pub fn from_utf8(input: &[u8]) -> Option> { - Self::from_string(String::borrow_utf8(input)) - } - - pub fn from_string(str: String) -> Option> { - let mut input = str; - NonNull::new(URL__fromString(&mut input)) - } - // from_js/from_string/from_utf8 return an owned C++ heap pointer that the - // caller must destroy(). - - pub fn protocol(&self) -> String { - URL__protocol(self) - } - - pub fn href(&self) -> String { - URL__href(self) - } - - pub fn username(&self) -> String { - URL__username(self) - } - - pub fn password(&self) -> String { - URL__password(self) - } - - pub fn search(&self) -> String { - URL__search(self) - } - - /// Returns the host WITHOUT the port. - /// - /// Note that this does NOT match JS behavior, which returns the host with the port. See - /// `hostname` for the JS equivalent of `host`. - /// - /// ```text - /// URL("http://example.com:8080").host() => "example.com" - /// ``` - pub fn host(&self) -> String { - URL__host(self) - } - - /// Returns the host WITH the port. - /// - /// Note that this does NOT match JS behavior which returns the host without the port. See - /// `host` for the JS equivalent of `hostname`. - /// - /// ```text - /// URL("http://example.com:8080").hostname() => "example.com:8080" - /// ``` - pub fn hostname(&self) -> String { - URL__hostname(self) - } - - /// Returns `u32::MAX` if the port is not set. Otherwise, `port` - /// is guaranteed to be within the `u16` range. - pub fn port(&self) -> u32 { - URL__port(self) - } - - // Kept as explicit destroy (not Drop) — URL is an opaque #[repr(C)] FFI - // handle constructed/destroyed across the C++ boundary. - pub unsafe fn destroy(this: *mut Self) { - // SAFETY: `this` is a valid *URL from C++; freed exactly once - unsafe { URL__deinit(this) } - } - - pub fn pathname(&self) -> String { - URL__pathname(self) - } } diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index e847b6a66271..4c91a9ea7d78 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -2379,17 +2379,14 @@ impl VirtualMachine { } } - /// `loadEntryPoint(entry_path)` — `reload_entry_point` + spin until the - /// returned promise settles. - pub fn load_entry_point( - &mut self, - entry_path: &[u8], - ) -> Result<*mut JSInternalPromise, bun_core::Error> { - let promise = self.reload_entry_point(entry_path)?; - + /// Shared wait body of [`load_entry_point`](Self::load_entry_point) / + /// [`load_entry_point_for_test_runner`](Self::load_entry_point_for_test_runner): + /// spin the event loop until the entry-point promise settles. Returns + /// `true` when `promise` was already rejected before waiting — callers + /// return it as-is, skipping their trailing tick/unwrap. + fn wait_for_entry_point_promise(&mut self, promise: *mut JSInternalPromise) -> bool { // pending_internal_promise can change if hot module reloading is enabled if self.is_watcher_enabled() { - // accessed here (no overlapping `&mut EventLoop`). self.event_loop_mut().perform_gc(); loop { let Some(p) = self.pending_internal_promise else { @@ -2411,12 +2408,24 @@ impl VirtualMachine { } else { // SAFETY: `promise` is a live JSC heap cell. if crate::JSPromise::status_ptr(promise) == crate::js_promise::Status::Rejected { - return Ok(promise); + return true; } self.event_loop_mut().perform_gc(); self.wait_for_promise(jsc::AnyPromise::Internal(promise)); } + false + } + /// `loadEntryPoint(entry_path)` — `reload_entry_point` + spin until the + /// returned promise settles. + pub fn load_entry_point( + &mut self, + entry_path: &[u8], + ) -> Result<*mut JSInternalPromise, bun_core::Error> { + let promise = self.reload_entry_point(entry_path)?; + if self.wait_for_entry_point_promise(promise) { + return Ok(promise); + } Ok(self.pending_internal_promise.unwrap_or(promise)) } @@ -4562,36 +4571,9 @@ impl VirtualMachine { entry_path: &[u8], ) -> Result<*mut JSInternalPromise, bun_core::Error> { let promise = self.reload_entry_point_for_test_runner(entry_path)?; - - // pending_internal_promise can change if hot module reloading is enabled - if self.is_watcher_enabled() { - self.event_loop_mut().perform_gc(); - loop { - let Some(p) = self.pending_internal_promise else { - break; - }; - // SAFETY: `p` is a live JSC heap cell tracked by the VM. - if crate::JSPromise::status_ptr(p) != crate::js_promise::Status::Pending { - break; - } - self.event_loop_mut().tick(); - let Some(p) = self.pending_internal_promise else { - break; - }; - // SAFETY: see above. - if crate::JSPromise::status_ptr(p) == crate::js_promise::Status::Pending { - self.auto_tick(); - } - } - } else { - // SAFETY: `promise` is a live JSC heap cell. - if crate::JSPromise::status_ptr(promise) == crate::js_promise::Status::Rejected { - return Ok(promise); - } - self.event_loop_mut().perform_gc(); - self.wait_for_promise(jsc::AnyPromise::Internal(promise)); + if self.wait_for_entry_point_promise(promise) { + return Ok(promise); } - self.auto_tick(); Ok(self.pending_internal_promise.unwrap()) } @@ -5593,7 +5575,7 @@ impl VirtualMachine { ) -> Result<(), bun_core::Error> { use crate::JSType; use crate::console_object::formatter::TagOptions; - use crate::console_object::{self, Tag, TagPayload}; + use crate::console_object::{self, Tag}; let prev_had_errors = self.had_errors; self.had_errors = true; @@ -6065,7 +6047,7 @@ impl VirtualMachine { global_ref, TagOptions::DISABLE_INSPECT_CUSTOM | TagOptions::HIDE_GLOBAL, )?; - if !matches!(tag.tag, TagPayload::NativeCode) { + if !matches!(tag.tag, Tag::NativeCode) { let _ = if allow_ansi_color { formatter.format::(tag, writer, error_instance, global_ref) } else { diff --git a/src/jsc/ipc.rs b/src/jsc/ipc.rs index 54a91536dbd1..97f6e7a4a1f2 100644 --- a/src/jsc/ipc.rs +++ b/src/jsc/ipc.rs @@ -1879,6 +1879,89 @@ fn handle_ipc_message( } } +/// Handles every decode failure other than `NotEnoughBytes` (which each call +/// site recovers from differently): report OOM, then close the socket. +#[inline] +fn close_socket_on_decode_failure(send_queue: &mut SendQueue, err: &IPCDecodeError) { + debug_assert!(!matches!(err, IPCDecodeError::NotEnoughBytes)); + if matches!(err, IPCDecodeError::OutOfMemory) { + Output::print_errorln("IPC message is too long."); + } + send_queue.close_socket(CloseReason::Failure, CloseFrom::User); +} + +/// Drains complete JSON-mode messages from `send_queue.incoming` (which must +/// be `IncomingBuffer::Json`). Shared by the POSIX `on_data` and Windows +/// libuv `on_read` callbacks. +fn drain_json_messages(send_queue: &mut SendQueue, global_this: &JSGlobalObject) { + loop { + let IncomingBuffer::Json(json_buf) = &mut send_queue.incoming else { + unreachable!() + }; + let Some(msg) = json_buf.next() else { break }; + let result = + match decode_ipc_message(Mode::Json, msg.data, global_this, Some(msg.newline_pos)) { + Ok(r) => r, + Err(IPCDecodeError::NotEnoughBytes) => { + log!("hit NotEnoughBytes"); + return; + } + Err(err) => { + close_socket_on_decode_failure(send_queue, &err); + return; + } + }; + + let bytes_consumed = result.bytes_consumed; + handle_ipc_message(send_queue, result.message, global_this); + let IncomingBuffer::Json(json_buf) = &mut send_queue.incoming else { + unreachable!() + }; + json_buf.consume(bytes_consumed); + } +} + +/// Drains complete Advanced-mode messages from the buffered bytes in +/// `send_queue.incoming` (which must be `IncomingBuffer::Advanced`). Shared by +/// the POSIX `on_data` and Windows libuv `on_read` callbacks. The buffer never +/// grows during the loop (no re-entrant reads inside `handle_ipc_message`), so +/// re-slicing from `slice_start` each iteration sees a stable tail. +fn drain_advanced_messages(send_queue: &mut SendQueue, global_this: &JSGlobalObject) { + let mut slice_start: usize = 0; + loop { + let IncomingBuffer::Advanced(adv_buf) = &mut send_queue.incoming else { + unreachable!() + }; + let slice = &adv_buf.slice()[slice_start..]; + let result = match decode_ipc_message(Mode::Advanced, slice, global_this, None) { + Ok(r) => r, + Err(IPCDecodeError::NotEnoughBytes) => { + // copy the remaining bytes to the start of the buffer + adv_buf.drain_front(slice_start); + log!("hit NotEnoughBytes2"); + return; + } + Err(err) => { + close_socket_on_decode_failure(send_queue, &err); + return; + } + }; + + let slice_len = slice.len(); + handle_ipc_message(send_queue, result.message, global_this); + + if (result.bytes_consumed as usize) < slice_len { + slice_start += result.bytes_consumed as usize; + } else { + let IncomingBuffer::Advanced(adv_buf) = &mut send_queue.incoming else { + unreachable!() + }; + adv_buf.clear(); + return; + } + } +} + fn on_data2(send_queue: &mut SendQueue, all_data: &[u8]) { let mut data = all_data; @@ -1898,45 +1981,7 @@ fn on_data2(send_queue: &mut SendQueue, all_data: &[u8]) { unreachable!() }; json_buf.append(data); - - loop { - let IncomingBuffer::Json(json_buf) = &mut send_queue.incoming else { - unreachable!() - }; - let Some(msg) = json_buf.next() else { break }; - let result = match decode_ipc_message( - Mode::Json, - msg.data, - &global_this, - Some(msg.newline_pos), - ) { - Ok(r) => r, - Err(IPCDecodeError::NotEnoughBytes) => { - log!("hit NotEnoughBytes"); - return; - } - Err( - IPCDecodeError::InvalidFormat - | IPCDecodeError::JSError - | IPCDecodeError::JSTerminated, - ) => { - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); - return; - } - Err(IPCDecodeError::OutOfMemory) => { - Output::print_errorln("IPC message is too long."); - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); - return; - } - }; - - let bytes_consumed = result.bytes_consumed; - handle_ipc_message(send_queue, result.message, &global_this); - let IncomingBuffer::Json(json_buf) = &mut send_queue.incoming else { - unreachable!() - }; - json_buf.consume(bytes_consumed); - } + drain_json_messages(send_queue, &global_this); } IncomingBuffer::Advanced(_) => { // Advanced mode: uses length-prefix, no newline scanning needed. @@ -1957,17 +2002,8 @@ fn on_data2(send_queue: &mut SendQueue, all_data: &[u8]) { log!("hit NotEnoughBytes"); return; } - Err( - IPCDecodeError::InvalidFormat - | IPCDecodeError::JSError - | IPCDecodeError::JSTerminated, - ) => { - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); - return; - } - Err(IPCDecodeError::OutOfMemory) => { - Output::print_errorln("IPC message is too long."); - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); + Err(err) => { + close_socket_on_decode_failure(send_queue, &err); return; } }; @@ -1987,48 +2023,7 @@ fn on_data2(send_queue: &mut SendQueue, all_data: &[u8]) { unreachable!() }; handle_oom(adv_buf.write(data)); - let mut slice_start: usize = 0; - loop { - let IncomingBuffer::Advanced(adv_buf) = &mut send_queue.incoming else { - unreachable!() - }; - let slice = &adv_buf.slice()[slice_start..]; - let result = match decode_ipc_message(Mode::Advanced, slice, &global_this, None) { - Ok(r) => r, - Err(IPCDecodeError::NotEnoughBytes) => { - // copy the remaining bytes to the start of the buffer - adv_buf.drain_front(slice_start); - log!("hit NotEnoughBytes2"); - return; - } - Err( - IPCDecodeError::InvalidFormat - | IPCDecodeError::JSError - | IPCDecodeError::JSTerminated, - ) => { - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); - return; - } - Err(IPCDecodeError::OutOfMemory) => { - Output::print_errorln("IPC message is too long."); - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); - return; - } - }; - - let slice_len = slice.len(); - handle_ipc_message(send_queue, result.message, &global_this); - - if (result.bytes_consumed as usize) < slice_len { - slice_start += result.bytes_consumed as usize; - } else { - let IncomingBuffer::Advanced(adv_buf) = &mut send_queue.incoming else { - unreachable!() - }; - adv_buf.clear(); - return; - } - } + drain_advanced_messages(send_queue, &global_this); } } } @@ -2167,46 +2162,7 @@ pub mod IPCHandlers { // and handing it to a `&mut self` method would alias // `json_buf.data`, undoing the Stacked-Borrows fix above. json_buf.notify_written(nread); - - // Process complete messages using next() - avoids O(n²) re-scanning - loop { - let IncomingBuffer::Json(json_buf) = &mut send_queue.incoming else { - unreachable!() - }; - let Some(msg) = json_buf.next() else { break }; - let result = match decode_ipc_message( - Mode::Json, - msg.data, - &global_this, - Some(msg.newline_pos), - ) { - Ok(r) => r, - Err(IPCDecodeError::NotEnoughBytes) => { - log!("hit NotEnoughBytes3"); - return; - } - Err( - IPCDecodeError::InvalidFormat - | IPCDecodeError::JSError - | IPCDecodeError::JSTerminated, - ) => { - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); - return; - } - Err(IPCDecodeError::OutOfMemory) => { - Output::print_errorln("IPC message is too long."); - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); - return; - } - }; - - let bytes_consumed = result.bytes_consumed; - handle_ipc_message(send_queue, result.message, &global_this); - let IncomingBuffer::Json(json_buf) = &mut send_queue.incoming else { - unreachable!() - }; - json_buf.consume(bytes_consumed); - } + drain_json_messages(send_queue, &global_this); } IncomingBuffer::Advanced(_) => { let IncomingBuffer::Advanced(adv_buf) = &mut send_queue.incoming else { @@ -2214,54 +2170,7 @@ pub mod IPCHandlers { }; // SAFETY: `on_read_alloc` reserved ≥ nread bytes; libuv initialised them. unsafe { adv_buf.uv_commit(nread) }; - let total_len = adv_buf.len(); - let mut slice_start: usize = 0; - - loop { - let IncomingBuffer::Advanced(adv_buf) = &mut send_queue.incoming else { - unreachable!() - }; - let slice = &adv_buf.slice()[slice_start..total_len]; - let result = - match decode_ipc_message(Mode::Advanced, slice, &global_this, None) { - Ok(r) => r, - Err(IPCDecodeError::NotEnoughBytes) => { - // copy the remaining bytes to the start of the buffer - // `total_len == adv_buf.len()` (captured post-uv_commit, never - // grown in this loop) ⇒ exact `len - slice_start` truncate. - adv_buf.drain_front(slice_start); - log!("hit NotEnoughBytes3"); - return; - } - Err( - IPCDecodeError::InvalidFormat - | IPCDecodeError::JSError - | IPCDecodeError::JSTerminated, - ) => { - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); - return; - } - Err(IPCDecodeError::OutOfMemory) => { - Output::print_errorln("IPC message is too long."); - send_queue.close_socket(CloseReason::Failure, CloseFrom::User); - return; - } - }; - - let slice_len = slice.len(); - handle_ipc_message(send_queue, result.message, &global_this); - - if (result.bytes_consumed as usize) < slice_len { - slice_start += result.bytes_consumed as usize; - } else { - // clear the buffer - let IncomingBuffer::Advanced(adv_buf) = &mut send_queue.incoming else { - unreachable!() - }; - adv_buf.clear(); - return; - } - } + drain_advanced_messages(send_queue, &global_this); } } } diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index f1e9717a439c..eab9c29c272d 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -393,8 +393,9 @@ impl<'a> ConsoleFormatter for self::console_object::Formatter<'a> { // the const-generic `print_as::<{ Tag::… }, …>` arms. let mut sink = bun_io::FmtAdapter::new(writer); let result = self::console_object::formatter::TagResult { - tag: tag.into(), + tag, cell, + custom: None, }; let global = self.global_this; self.format::(result, &mut sink, value, global) @@ -987,7 +988,7 @@ mod __macro_smoke { pub use self::cached_bytecode::CachedBytecode; pub use self::deferred_error::DeferredError; pub use self::dom_form_data::DOMFormData; -pub use self::url::URL; +pub use self::url::{URL, UrlJsc}; pub use self::zig_stack_frame::ZigStackFrame; pub use self::zig_stack_trace::ZigStackTrace; pub use abort_signal::{AbortSignal, AbortSignalRef}; @@ -1480,8 +1481,8 @@ impl FromJsEnum for bun_http_types::FetchCacheMode::FetchCacheMode { } } -// `URL::path_from_file_url` / `URL::href_from_js` live in `URL.rs` (the -// dedicated port file); the lib.rs copies were duplicate definitions. +// `URL` is a re-export of `bun_url::whatwg::URL`; the JS-value entry points +// (`UrlJsc::from_js` / `UrlJsc::href_from_js`) live in `URL.rs`. // JSString (real module in JSString.rs). #[path = "JSString.rs"] diff --git a/src/parsers/json.rs b/src/parsers/json.rs index 7771dd9126a4..d8d6c7060ef7 100644 --- a/src/parsers/json.rs +++ b/src/parsers/json.rs @@ -811,6 +811,39 @@ fn empty_array_data() -> js_ast::expr::Data { js_ast::expr::Data::EArray(js_ast::StoreRef::from_raw(EMPTY_ARRAY.get())) } +/// Shared fast-path prologue for every JSON entry point: empty input parses +/// as an empty object (consistent with how disabled JS files are handled), +/// and two-byte `""`/`''`/`{}`/`[]` inputs skip the lexer entirely. +/// +/// Note: the two-byte arms compare a one-byte slice (`contents[0..1]`) +/// against two-byte literals, so they never match. This mirrors the Zig +/// reference (`json.zig` does the same with `eqlComptime`) — kept as-is to +/// preserve behavior, since "fixing" it would accept `''` in strict JSON. +#[inline] +fn empty_source_fast_path(source: &bun_ast::Source) -> Option { + let expr = |data| { + Some(Expr { + loc: bun_ast::Loc { start: 0 }, + data, + }) + }; + match source.contents.len() { + 0 => expr(empty_object_data()), + 2 => { + if &source.contents[0..1] == b"\"\"" || &source.contents[0..1] == b"''" { + expr(empty_string_data()) + } else if &source.contents[0..1] == b"{}" { + expr(empty_object_data()) + } else if &source.contents[0..1] == b"[]" { + expr(empty_array_data()) + } else { + None + } + } + _ => None, + } +} + // ────────────────────────────────────────────────────────────────────────── /// Parse JSON @@ -827,37 +860,12 @@ pub fn parse( log: &mut bun_ast::Log, bump: &Bump, ) -> Result { - let mut parser = JSONLikeParser::init(JSON_OPTS, bump, source, log)?; - match source.contents.len() { - // This is to be consisntent with how disabled JS files are handled - 0 => { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_object_data(), - }); - } - // This is a fast pass I guess - 2 => { - if &source.contents[0..1] == b"\"\"" || &source.contents[0..1] == b"''" { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_string_data(), - }); - } else if &source.contents[0..1] == b"{}" { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_object_data(), - }); - } else if &source.contents[0..1] == b"[]" { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_array_data(), - }); - } - } - _ => {} + if let Some(expr) = empty_source_fast_path(source) { + return Ok(expr); } + let mut parser = JSONLikeParser::init(JSON_OPTS, bump, source, log)?; + parser.parse_expr(false, FORCE_UTF8) } @@ -871,36 +879,8 @@ pub fn parse_package_json_utf8( log: &mut bun_ast::Log, bump: &Bump, ) -> Result { - let len = source.contents.len(); - - match len { - // This is to be consisntent with how disabled JS files are handled - 0 => { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_object_data(), - }); - } - // This is a fast pass I guess - 2 => { - if &source.contents[0..1] == b"\"\"" || &source.contents[0..1] == b"''" { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_string_data(), - }); - } else if &source.contents[0..1] == b"{}" { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_object_data(), - }); - } else if &source.contents[0..1] == b"[]" { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_array_data(), - }); - } - } - _ => {} + if let Some(expr) = empty_source_fast_path(source) { + return Ok(expr); } let mut parser = JSONLikeParser::init(PACKAGE_JSON_OPTS, bump, source, log)?; @@ -961,48 +941,11 @@ pub fn parse_package_json_utf8_with_opts_rt( log: &mut bun_ast::Log, bump: &Bump, ) -> Result { - let len = source.contents.len(); - - match len { - // This is to be consisntent with how disabled JS files are handled - 0 => { - return Ok(JsonResult { - root: Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_object_data(), - }, - indentation: Indentation::default(), - }); - } - // This is a fast pass I guess - 2 => { - if &source.contents[0..1] == b"\"\"" || &source.contents[0..1] == b"''" { - return Ok(JsonResult { - root: Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_string_data(), - }, - indentation: Indentation::default(), - }); - } else if &source.contents[0..1] == b"{}" { - return Ok(JsonResult { - root: Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_object_data(), - }, - indentation: Indentation::default(), - }); - } else if &source.contents[0..1] == b"[]" { - return Ok(JsonResult { - root: Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_array_data(), - }, - indentation: Indentation::default(), - }); - } - } - _ => {} + if let Some(root) = empty_source_fast_path(source) { + return Ok(JsonResult { + root, + indentation: Indentation::default(), + }); } let mut parser = JSONLikeParser::init(opts, bump, source, log)?; @@ -1039,36 +982,8 @@ pub fn parse_utf8_impl( log: &mut bun_ast::Log, bump: &Bump, ) -> Result { - let len = source.contents.len(); - - match len { - // This is to be consisntent with how disabled JS files are handled - 0 => { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_object_data(), - }); - } - // This is a fast pass I guess - 2 => { - if &source.contents[0..1] == b"\"\"" || &source.contents[0..1] == b"''" { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_string_data(), - }); - } else if &source.contents[0..1] == b"{}" { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_object_data(), - }); - } else if &source.contents[0..1] == b"[]" { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_array_data(), - }); - } - } - _ => {} + if let Some(expr) = empty_source_fast_path(source) { + return Ok(expr); } let mut parser = JSONLikeParser::init(JSON_OPTS, bump, source, log)?; @@ -1090,34 +1005,8 @@ pub fn parse_for_macro( log: &mut bun_ast::Log, bump: &Bump, ) -> Result { - match source.contents.len() { - // This is to be consisntent with how disabled JS files are handled - 0 => { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_object_data(), - }); - } - // This is a fast pass I guess - 2 => { - if &source.contents[0..1] == b"\"\"" || &source.contents[0..1] == b"''" { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_string_data(), - }); - } else if &source.contents[0..1] == b"{}" { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_object_data(), - }); - } else if &source.contents[0..1] == b"[]" { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_array_data(), - }); - } - } - _ => {} + if let Some(expr) = empty_source_fast_path(source) { + return Ok(expr); } let mut parser = JSONLikeParser::init(MACRO_JSON_OPTS, bump, source, log)?; @@ -1143,46 +1032,15 @@ pub fn parse_for_bundling( log: &mut bun_ast::Log, bump: &Bump, ) -> Result { - match source.contents.len() { - // This is to be consisntent with how disabled JS files are handled - 0 => { - return Ok(JSONParseResult { - expr: Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_object_data(), - }, - tag: JSONParseResultTag::Empty, - }); - } - // This is a fast pass I guess - 2 => { - if &source.contents[0..1] == b"\"\"" || &source.contents[0..1] == b"''" { - return Ok(JSONParseResult { - expr: Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_string_data(), - }, - tag: JSONParseResultTag::Expr, - }); - } else if &source.contents[0..1] == b"{}" { - return Ok(JSONParseResult { - expr: Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_object_data(), - }, - tag: JSONParseResultTag::Expr, - }); - } else if &source.contents[0..1] == b"[]" { - return Ok(JSONParseResult { - expr: Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_array_data(), - }, - tag: JSONParseResultTag::Expr, - }); - } - } - _ => {} + if let Some(expr) = empty_source_fast_path(source) { + return Ok(JSONParseResult { + expr, + tag: if source.contents.is_empty() { + JSONParseResultTag::Empty + } else { + JSONParseResultTag::Expr + }, + }); } let mut parser = JSONLikeParser::init(JSON_OPTS, bump, source, log)?; @@ -1204,34 +1062,8 @@ pub fn parse_env_json( log: &mut bun_ast::Log, bump: &Bump, ) -> Result { - match source.contents.len() { - // This is to be consisntent with how disabled JS files are handled - 0 => { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_object_data(), - }); - } - // This is a fast pass I guess - 2 => { - if &source.contents[0..1] == b"\"\"" || &source.contents[0..1] == b"''" { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_string_data(), - }); - } else if &source.contents[0..1] == b"{}" { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_object_data(), - }); - } else if &source.contents[0..1] == b"[]" { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_array_data(), - }); - } - } - _ => {} + if let Some(expr) = empty_source_fast_path(source) { + return Ok(expr); } let mut parser = JSONLikeParser::init(DOTENV_JSON_OPTS, bump, source, log)?; @@ -1272,34 +1104,8 @@ pub fn parse_ts_config( log: &mut bun_ast::Log, bump: &Bump, ) -> Result { - match source.contents.len() { - // This is to be consisntent with how disabled JS files are handled - 0 => { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_object_data(), - }); - } - // This is a fast pass I guess - 2 => { - if &source.contents[0..1] == b"\"\"" || &source.contents[0..1] == b"''" { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_string_data(), - }); - } else if &source.contents[0..1] == b"{}" { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_object_data(), - }); - } else if &source.contents[0..1] == b"[]" { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: empty_array_data(), - }); - } - } - _ => {} + if let Some(expr) = empty_source_fast_path(source) { + return Ok(expr); } let mut parser = JSONLikeParser::init(TSCONFIG_OPTS, bump, source, log)?; diff --git a/src/parsers/json_lexer.rs b/src/parsers/json_lexer.rs index 1b5954bd4038..5f53dee8e907 100644 --- a/src/parsers/json_lexer.rs +++ b/src/parsers/json_lexer.rs @@ -222,6 +222,28 @@ impl<'a, 'bump> LexerLog<'a> for Lexer<'a, 'bump> { } } +impl<'a, 'bump> crate::number_scan::DecimalLexer<'a> for Lexer<'a, 'bump> +where + 'bump: 'a, +{ + #[inline] + fn code_point(&self) -> CodePoint { + self.code_point + } + #[inline] + fn end(&self) -> usize { + self.end + } + #[inline] + fn end_mut(&mut self) -> &mut usize { + &mut self.end + } + #[inline] + fn step(&mut self) { + Lexer::step(self) + } +} + impl<'a, 'bump> Lexer<'a, 'bump> where // `identifier` may point into `source.contents` (`'a`) *or* a bump-alloc'd @@ -697,7 +719,6 @@ where return self.syntax_error(); } - let mut underscore_count: usize = 0; let mut last_underscore_end: usize = 0; let mut has_dot_or_exponent = first == '.' as CodePoint; let mut base: f64 = 0.0; @@ -818,87 +839,10 @@ where } } else { // Floating-point literal; - let is_invalid_legacy_octal_literal = first == '0' as CodePoint - && (self.code_point == '8' as CodePoint || self.code_point == '9' as CodePoint); - - // Initial digits; - loop { - if self.code_point < '0' as CodePoint || self.code_point > '9' as CodePoint { - if self.code_point != '_' as CodePoint { - break; - } - // Cannot have multiple underscores in a row; - if last_underscore_end > 0 && self.end == last_underscore_end + 1 { - self.syntax_error()?; - } - // The specification forbids underscores in this case; - if is_invalid_legacy_octal_literal { - self.syntax_error()?; - } - last_underscore_end = self.end; - underscore_count += 1; - } - self.step(); - } - - // Fractional digits; - if first != '.' as CodePoint && self.code_point == '.' as CodePoint { - // An underscore must not come last; - if last_underscore_end > 0 && self.end == last_underscore_end + 1 { - self.end -= 1; - self.syntax_error()?; - } - has_dot_or_exponent = true; - self.step(); - if self.code_point == '_' as CodePoint { - self.syntax_error()?; - } - loop { - if self.code_point < '0' as CodePoint || self.code_point > '9' as CodePoint { - if self.code_point != '_' as CodePoint { - break; - } - // Cannot have multiple underscores in a row; - if last_underscore_end > 0 && self.end == last_underscore_end + 1 { - self.syntax_error()?; - } - last_underscore_end = self.end; - underscore_count += 1; - } - self.step(); - } - } - - // Exponent; - if self.code_point == 'e' as CodePoint || self.code_point == 'E' as CodePoint { - // An underscore must not come last; - if last_underscore_end > 0 && self.end == last_underscore_end + 1 { - self.end -= 1; - self.syntax_error()?; - } - has_dot_or_exponent = true; - self.step(); - if self.code_point == '+' as CodePoint || self.code_point == '-' as CodePoint { - self.step(); - } - if self.code_point < '0' as CodePoint || self.code_point > '9' as CodePoint { - self.syntax_error()?; - } - loop { - if self.code_point < '0' as CodePoint || self.code_point > '9' as CodePoint { - if self.code_point != '_' as CodePoint { - break; - } - // Cannot have multiple underscores in a row; - if last_underscore_end > 0 && self.end == last_underscore_end + 1 { - self.syntax_error()?; - } - last_underscore_end = self.end; - underscore_count += 1; - } - self.step(); - } - } + let scan = crate::number_scan::scan_decimal_digits(self, first)?; + let underscore_count = scan.underscore_count; + last_underscore_end = scan.last_underscore_end; + has_dot_or_exponent = scan.has_dot_or_exponent; // Take a slice of the text to parse; let text = self.raw(); diff --git a/src/parsers/lib.rs b/src/parsers/lib.rs index e5fb1f707a7c..997bff8c0228 100644 --- a/src/parsers/lib.rs +++ b/src/parsers/lib.rs @@ -8,6 +8,10 @@ // Crate-private: implementation detail of `json.rs`; no external consumers. mod json_lexer; +// ───── number_scan ──────────────────────────────────────────────────────── +// Decimal number-literal digit scanner shared by the json and toml lexers. +mod number_scan; + // ───── json ─────────────────────────────────────────────────────────────── // Real port — wired against `crate::json_lexer` (the cycle-break above) and // `bun_ast::js_ast`; resolves against the local lexer so `bun_js_parser` diff --git a/src/parsers/number_scan.rs b/src/parsers/number_scan.rs new file mode 100644 index 000000000000..1b900d50a015 --- /dev/null +++ b/src/parsers/number_scan.rs @@ -0,0 +1,128 @@ +//! Shared decimal number-literal digit scanner. +//! +//! The json and toml lexers each carried an identical ~80-line scan of a +//! decimal literal's digits — underscore-separator rules, optional fraction, +//! optional exponent, and the invalid-legacy-octal underscore check (see the +//! matching regions in `js_parser/lexer.zig` and `parsers/toml/lexer.zig`). +//! This generic helper collapses both; it monomorphizes per lexer type, so +//! codegen matches the previous inline copies. + +use bun_ast::LexerLog; +use bun_core::strings::CodePoint; + +/// Cursor surface `scan_decimal_digits` needs from a lexer. +pub(crate) trait DecimalLexer<'s>: LexerLog<'s> { + fn code_point(&self) -> CodePoint; + fn end(&self) -> usize; + fn end_mut(&mut self) -> &mut usize; + fn step(&mut self); +} + +pub(crate) struct DecimalScan { + pub underscore_count: usize, + pub last_underscore_end: usize, + pub has_dot_or_exponent: bool, +} + +/// Scans the digits of a decimal (non-radix-prefixed) number literal: +/// initial digits, then an optional fraction and exponent. The caller has +/// already consumed `first` (the literal's first code point); on return the +/// cursor sits on the first code point past the literal and the caller +/// parses `lexer.raw()` into a value. +#[inline] +pub(crate) fn scan_decimal_digits<'s, L: DecimalLexer<'s>>( + lexer: &mut L, + first: CodePoint, +) -> Result { + let mut underscore_count: usize = 0; + let mut last_underscore_end: usize = 0; + let mut has_dot_or_exponent = first == '.' as CodePoint; + + let is_invalid_legacy_octal_literal = first == '0' as CodePoint + && (lexer.code_point() == '8' as CodePoint || lexer.code_point() == '9' as CodePoint); + + // Initial digits; + loop { + if lexer.code_point() < '0' as CodePoint || lexer.code_point() > '9' as CodePoint { + if lexer.code_point() != '_' as CodePoint { + break; + } + // Cannot have multiple underscores in a row; + if last_underscore_end > 0 && lexer.end() == last_underscore_end + 1 { + lexer.syntax_error()?; + } + // The specification forbids underscores in this case; + if is_invalid_legacy_octal_literal { + lexer.syntax_error()?; + } + last_underscore_end = lexer.end(); + underscore_count += 1; + } + lexer.step(); + } + + // Fractional digits; + if first != '.' as CodePoint && lexer.code_point() == '.' as CodePoint { + // An underscore must not come last; + if last_underscore_end > 0 && lexer.end() == last_underscore_end + 1 { + *lexer.end_mut() -= 1; + lexer.syntax_error()?; + } + has_dot_or_exponent = true; + lexer.step(); + if lexer.code_point() == '_' as CodePoint { + lexer.syntax_error()?; + } + loop { + if lexer.code_point() < '0' as CodePoint || lexer.code_point() > '9' as CodePoint { + if lexer.code_point() != '_' as CodePoint { + break; + } + // Cannot have multiple underscores in a row; + if last_underscore_end > 0 && lexer.end() == last_underscore_end + 1 { + lexer.syntax_error()?; + } + last_underscore_end = lexer.end(); + underscore_count += 1; + } + lexer.step(); + } + } + + // Exponent; + if lexer.code_point() == 'e' as CodePoint || lexer.code_point() == 'E' as CodePoint { + // An underscore must not come last; + if last_underscore_end > 0 && lexer.end() == last_underscore_end + 1 { + *lexer.end_mut() -= 1; + lexer.syntax_error()?; + } + has_dot_or_exponent = true; + lexer.step(); + if lexer.code_point() == '+' as CodePoint || lexer.code_point() == '-' as CodePoint { + lexer.step(); + } + if lexer.code_point() < '0' as CodePoint || lexer.code_point() > '9' as CodePoint { + lexer.syntax_error()?; + } + loop { + if lexer.code_point() < '0' as CodePoint || lexer.code_point() > '9' as CodePoint { + if lexer.code_point() != '_' as CodePoint { + break; + } + // Cannot have multiple underscores in a row; + if last_underscore_end > 0 && lexer.end() == last_underscore_end + 1 { + lexer.syntax_error()?; + } + last_underscore_end = lexer.end(); + underscore_count += 1; + } + lexer.step(); + } + } + + Ok(DecimalScan { + underscore_count, + last_underscore_end, + has_dot_or_exponent, + }) +} diff --git a/src/parsers/toml/lexer.rs b/src/parsers/toml/lexer.rs index cfe8c61a5cad..9c043b7495f8 100644 --- a/src/parsers/toml/lexer.rs +++ b/src/parsers/toml/lexer.rs @@ -2,7 +2,6 @@ use bun_alloc::Arena; // bumpalo::Bump re-export use bun_alloc::ArenaVecExt as _; use bun_ast as js_ast; use bun_ast::LexerLog; -use bun_core::fmt::hex_digit_value_u32; use bun_core::strings; use bun_core::strings::CodePoint; @@ -129,6 +128,44 @@ impl<'a> LexerLog<'a> for Lexer<'a> { } } +impl<'a> bun_ast::lexer_log::EscapeLexer<'a> for Lexer<'a> { + type Buf = bun_alloc::ArenaVec<'a, u8>; + const LEGACY_ERROR_SPANS: bool = true; + #[inline] + fn end_mut(&mut self) -> &mut usize { + &mut self.end + } + #[inline] + fn push_codepoint(buf: &mut Self::Buf, c: u32) { + if c <= 127 { + buf.push(c as u8); + } else { + let mut part: [u8; 4] = [0; 4]; + let len = strings::encode_wtf8_rune(&mut part, c); + buf.extend_from_slice(&part[0..len]); + } + } +} + +impl<'a> crate::number_scan::DecimalLexer<'a> for Lexer<'a> { + #[inline] + fn code_point(&self) -> CodePoint { + self.code_point + } + #[inline] + fn end(&self) -> usize { + self.end + } + #[inline] + fn end_mut(&mut self) -> &mut usize { + &mut self.end + } + #[inline] + fn step(&mut self) { + Lexer::step(self) + } +} + impl<'a> Lexer<'a> { #[inline] pub fn loc(&self) -> bun_ast::Loc { @@ -318,115 +355,9 @@ impl<'a> Lexer<'a> { } } else { // Floating-point literal; - let is_invalid_legacy_octal_literal = first == '0' as CodePoint - && (self.code_point == '8' as CodePoint || self.code_point == '9' as CodePoint); - - // Initial digits; - loop { - if self.code_point < '0' as CodePoint || self.code_point > '9' as CodePoint { - match self.code_point { - // '-' => { - // if (lexer.raw().len == 5) { - // // Is this possibly a datetime literal that begins with a 4 digit year? - // lexer.step(); - // while (!lexer.has_newline_before) { - // switch (lexer.code_point) { - // ',' => { - // lexer.string_literal_slice = lexer.raw(); - // lexer.token = T.t_string_literal; - // break; - // }, - // } - // } - // } - // }, - c if c == '_' as CodePoint => {} - _ => break, - } - if self.code_point != '_' as CodePoint { - break; - } - - // Cannot have multiple underscores in a row; - if last_underscore_end > 0 && self.end == last_underscore_end + 1 { - self.syntax_error()?; - } - - // The specification forbids underscores in this case; - if is_invalid_legacy_octal_literal { - self.syntax_error()?; - } - - last_underscore_end = self.end; - underscore_count += 1; - } - self.step(); - } - - // Fractional digits; - if first != '.' as CodePoint && self.code_point == '.' as CodePoint { - // An underscore must not come last; - if last_underscore_end > 0 && self.end == last_underscore_end + 1 { - self.end -= 1; - self.syntax_error()?; - } - - has_dot_or_exponent = true; - self.step(); - if self.code_point == '_' as CodePoint { - self.syntax_error()?; - } - loop { - if self.code_point < '0' as CodePoint || self.code_point > '9' as CodePoint { - if self.code_point != '_' as CodePoint { - break; - } - - // Cannot have multiple underscores in a row; - if last_underscore_end > 0 && self.end == last_underscore_end + 1 { - self.syntax_error()?; - } - - last_underscore_end = self.end; - underscore_count += 1; - } - self.step(); - } - } - - // Exponent; - if self.code_point == 'e' as CodePoint || self.code_point == 'E' as CodePoint { - // An underscore must not come last; - if last_underscore_end > 0 && self.end == last_underscore_end + 1 { - self.end -= 1; - self.syntax_error()?; - } - - has_dot_or_exponent = true; - self.step(); - if self.code_point == '+' as CodePoint || self.code_point == '-' as CodePoint { - self.step(); - } - if self.code_point < '0' as CodePoint || self.code_point > '9' as CodePoint { - self.syntax_error()?; - } - loop { - if self.code_point < '0' as CodePoint || self.code_point > '9' as CodePoint { - if self.code_point != '_' as CodePoint { - break; - } - - // Cannot have multiple underscores in a row; - if last_underscore_end > 0 && self.end == last_underscore_end + 1 { - self.syntax_error()?; - } - - last_underscore_end = self.end; - underscore_count += 1; - } - self.step(); - } - } + let scan = crate::number_scan::scan_decimal_digits(self, first)?; + underscore_count = scan.underscore_count; + has_dot_or_exponent = scan.has_dot_or_exponent; // Take a slice of the text to parse; let mut text: &[u8] = self.raw(); @@ -854,327 +785,11 @@ impl<'a> Lexer<'a> { text: &[u8], buf: &mut bun_alloc::ArenaVec<'a, u8>, ) -> Result<(), Error> { - let iterator = strings::CodepointIterator::init(text); - let mut iter = strings::Cursor::default(); - while iterator.next(&mut iter) { - let width = iter.width; - match iter.c { - c if c == '\r' as CodePoint => { - // Convert '\r\n' into '\n'. After `next()` returns for `\r`, - // `iter.i` is the start byte of the `\r` itself — the `\n` - // we're looking for is at `iter.i + 1`. Reading `text[iter.i]` - // would always be `\r`, so the check never fired and a literal - // CRLF in a slow-path multiline basic string decoded to two LFs. - // Match the JS lexer (js_parser/lexer.rs:660-661). - let next_i: usize = iter.i as usize + 1; - if next_i < text.len() && text[next_i] == b'\n' { - iter.i += 1; - } - - // Convert '\r' into '\n' - buf.push(b'\n'); - continue; - } - - c if c == '\\' as CodePoint => { - if !iterator.next(&mut iter) { - return Ok(()); - } - - let c2 = iter.c; - - let width2 = iter.width; - match c2 { - // https://mathiasbynens.be/notes/javascript-escapes#single - c if c == 'b' as CodePoint => { - buf.push(8); - continue; - } - c if c == 'f' as CodePoint => { - // Form feed: U+000C - buf.push(12); - continue; - } - c if c == 'n' as CodePoint => { - buf.push(10); - continue; - } - c if c == 'v' as CodePoint => { - // Vertical tab is invalid JSON - // We're going to allow it. - buf.push(11); - continue; - } - c if c == 't' as CodePoint => { - // Horizontal tab: U+0009 - buf.push(9); - continue; - } - c if c == 'r' as CodePoint => { - buf.push(13); - continue; - } - - // legacy octal literals - c if ('0' as CodePoint..='7' as CodePoint).contains(&c) => { - let octal_start = (iter.i as usize + width2 as usize).saturating_sub(2); - - // 1-3 digit octal - let mut is_bad = false; - let mut value: i64 = (c2 - '0' as CodePoint) as i64; - let mut restore = iter; - - if !iterator.next(&mut iter) { - if value == 0 { - buf.push(0); - return Ok(()); - } - - self.syntax_error()?; - return Ok(()); - } - - let c3: CodePoint = iter.c; - - match c3 { - c if ('0' as CodePoint..='7' as CodePoint).contains(&c) => { - value = value * 8 + (c3 - '0' as CodePoint) as i64; - restore = iter; - if !iterator.next(&mut iter) { - return self.syntax_error(); - } - - let c4 = iter.c; - match c4 { - c if ('0' as CodePoint..='7' as CodePoint).contains(&c) => { - let temp = value * 8 + (c4 - '0' as CodePoint) as i64; - if temp < 256 { - value = temp; - } else { - iter = restore; - } - } - c if c == '8' as CodePoint || c == '9' as CodePoint => { - is_bad = true; - } - _ => { - iter = restore; - } - } - } - c if c == '8' as CodePoint || c == '9' as CodePoint => { - is_bad = true; - } - _ => { - iter = restore; - } - } - - iter.c = i32::try_from(value).expect("int cast"); - if is_bad { - self.add_range_error( - bun_ast::Range { - loc: bun_ast::Loc { - start: i32::try_from(octal_start).expect("int cast"), - }, - len: i32::try_from(iter.i as usize - octal_start) - .expect("int cast"), - }, - format_args!("Invalid legacy octal literal"), - ) - .expect("unreachable"); - } - } - c if c == '8' as CodePoint || c == '9' as CodePoint => { - iter.c = c2; - } - // 2-digit hexadecimal - c if c == 'x' as CodePoint => { - if ALLOW_MULTILINE { - self.end = - (start + iter.i as usize).saturating_sub(width2 as usize); - self.syntax_error()?; - } - - let mut value: CodePoint = 0; - let mut c3: CodePoint; - let mut width3: u8; - - if !iterator.next(&mut iter) { - return self.syntax_error(); - } - c3 = iter.c; - width3 = iter.width; - match hex_digit_value_u32(c3 as u32) { - Some(d) => value = (value * 16) | d as CodePoint, - None => { - self.end = - (start + iter.i as usize).saturating_sub(width3 as usize); - return self.syntax_error(); - } - } - - if !iterator.next(&mut iter) { - return self.syntax_error(); - } - c3 = iter.c; - width3 = iter.width; - match hex_digit_value_u32(c3 as u32) { - Some(d) => value = (value * 16) | d as CodePoint, - None => { - self.end = - (start + iter.i as usize).saturating_sub(width3 as usize); - return self.syntax_error(); - } - } - - iter.c = value; - } - c if c == 'u' as CodePoint => { - // We're going to make this an i64 so we don't risk integer overflows - // when people do weird things - let mut value: i64 = 0; - - if !iterator.next(&mut iter) { - return self.syntax_error(); - } - let mut c3 = iter.c; - let mut width3 = iter.width; - - // variable-length - if c3 == '{' as CodePoint { - let hex_start = (iter.i as usize) - .saturating_sub(width as usize) - .saturating_sub(width2 as usize) - .saturating_sub(width3 as usize); - let mut is_first = true; - let mut is_out_of_range = false; - 'variable_length: loop { - if !iterator.next(&mut iter) { - break 'variable_length; - } - c3 = iter.c; - - if c3 == '}' as CodePoint { - if is_first { - self.end = (start + iter.i as usize) - .saturating_sub(width3 as usize); - return self.syntax_error(); - } - break 'variable_length; - } - match hex_digit_value_u32(c3 as u32) { - Some(d) => value = (value * 16) | d as i64, - None => { - self.end = (start + iter.i as usize) - .saturating_sub(width3 as usize); - return self.syntax_error(); - } - } - - // '\U0010FFFF - // copied from golang utf8.MaxRune - if value > 1114111 { - is_out_of_range = true; - } - is_first = false; - } - - if is_out_of_range { - self.add_range_error( - bun_ast::Range { - loc: bun_ast::Loc { - start: i32::try_from(start + hex_start) - .expect("int cast"), - }, - len: i32::try_from( - (iter.i as usize).saturating_sub(hex_start), - ) - .unwrap(), - }, - format_args!("Unicode escape sequence is out of range"), - )?; - return Ok(()); - } - - // fixed-length - } else { - // Fixed-length - let mut j: usize = 0; - while j < 4 { - match hex_digit_value_u32(c3 as u32) { - Some(d) => value = (value * 16) | d as i64, - None => { - self.end = (start + iter.i as usize) - .saturating_sub(width3 as usize); - return self.syntax_error(); - } - } - - if j < 3 { - if !iterator.next(&mut iter) { - return self.syntax_error(); - } - c3 = iter.c; - - width3 = iter.width; - } - j += 1; - } - } - - iter.c = value as CodePoint; // @truncate - } - c if c == '\r' as CodePoint => { - if !ALLOW_MULTILINE { - self.end = - (start + iter.i as usize).saturating_sub(width2 as usize); - self.add_default_error(b"Unexpected end of line")?; - } - - // Ignore line continuations. A line continuation is not an escaped newline. - // Match the JS lexer (js_parser/lexer.rs:660-661, 937-939): guard on - // the index we actually read (`iter.i + 1`), not `iter.i`. Without - // this, a multiline basic string ending in `\` right before `"""` - // reads `text[len]` and panics even in release (slice bounds checks - // always run). - let next_i: usize = iter.i as usize + 1; - if next_i < text.len() && text[next_i] == b'\n' { - // Make sure Windows CRLF counts as a single newline - iter.i += 1; - } - continue; - } - c if c == '\n' as CodePoint || c == 0x2028 || c == 0x2029 => { - // Ignore line continuations. A line continuation is not an escaped newline. - if !ALLOW_MULTILINE { - self.end = - (start + iter.i as usize).saturating_sub(width2 as usize); - self.add_default_error(b"Unexpected end of line")?; - } - continue; - } - _ => { - iter.c = c2; - } - } - } - _ => {} - } - - match iter.c { - -1 => return self.add_default_error(b"Unexpected end of file"), - 0..=127 => { - buf.push(u8::try_from(iter.c).expect("int cast")); - } - _ => { - let mut part: [u8; 4] = [0; 4]; - let len = strings::encode_wtf8_rune(&mut part, iter.c as u32); - buf.extend_from_slice(&part[0..len]); - } - } - } - Ok(()) + // Multiline basic strings permit line continuations but reject `\x`; + // single-line basic strings are the inverse. + bun_ast::lexer_log::decode_escape_sequences::<_, ALLOW_MULTILINE, ALLOW_MULTILINE>( + self, start, text, buf, + ) } pub fn expected(&mut self, token: T) -> Result<(), Error> { diff --git a/src/paths/lib.rs b/src/paths/lib.rs index b8598cfd244f..8552b263233b 100644 --- a/src/paths/lib.rs +++ b/src/paths/lib.rs @@ -684,12 +684,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] @@ -924,23 +918,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 8f9ec1cb2d0d..0c66d1e155bf 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 { @@ -1246,14 +1243,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/resolver/fs.rs b/src/resolver/fs.rs index c72ebe8c1b77..8379147371e9 100644 --- a/src/resolver/fs.rs +++ b/src/resolver/fs.rs @@ -21,128 +21,7 @@ use bun_threading::Mutex; // edition-2024 forbids fn params shadowing statics). bun_core::define_scoped_log!(debug, Fs, hidden); -// ── BOM ────────────────────────────────────────────────────────────────────── -// A `BOM` enum also -// lives in `bun_core::immutable::unicode_draft` but that module is private -// (`mod unicode_draft` — no `pub use` of `BOM` yet); the resolver needs it for -// `read_file_with_handle_and_allocator` so the enum is duplicated here. The -// UTF-16→UTF-8 transcode goes through `strings::to_utf8_alloc` (re-exported -// from `bun_core::strings`, simdutf-backed) — no C++ is reimplemented. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum BOM { - Utf8, - Utf16Le, - Utf16Be, - Utf32Le, - Utf32Be, -} - -impl BOM { - pub const UTF8_BYTES: [u8; 3] = [0xef, 0xbb, 0xbf]; - pub const UTF16_LE_BYTES: [u8; 2] = [0xff, 0xfe]; - pub const UTF16_BE_BYTES: [u8; 2] = [0xfe, 0xff]; - pub const UTF32_LE_BYTES: [u8; 4] = [0xff, 0xfe, 0x00, 0x00]; - pub const UTF32_BE_BYTES: [u8; 4] = [0x00, 0x00, 0xfe, 0xff]; - - pub fn detect(bytes: &[u8]) -> Option { - if bytes.len() < 3 { - return None; - } - if bytes.starts_with(&Self::UTF8_BYTES) { - return Some(BOM::Utf8); - } - if bytes.starts_with(&Self::UTF16_LE_BYTES) { - // if (bytes.len > 4 and eqlComptimeIgnoreLen(bytes[2..], utf32_le_bytes[2..])) - // return .utf32_le; - return Some(BOM::Utf16Le); - } - // if (eqlComptimeIgnoreLen(bytes, utf16_be_bytes)) return .utf16_be; - // if (bytes.len > 4 and eqlComptimeIgnoreLen(bytes, utf32_le_bytes)) return .utf32_le; - None - } - - pub fn header(self) -> &'static [u8] { - match self { - BOM::Utf8 => &Self::UTF8_BYTES, - BOM::Utf16Le => &Self::UTF16_LE_BYTES, - BOM::Utf16Be => &Self::UTF16_BE_BYTES, - BOM::Utf32Le => &Self::UTF32_LE_BYTES, - BOM::Utf32Be => &Self::UTF32_BE_BYTES, - } - } - - pub fn tag_name(self) -> &'static str { - match self { - BOM::Utf8 => "utf8", - BOM::Utf16Le => "utf16_le", - BOM::Utf16Be => "utf16_be", - BOM::Utf32Le => "utf32_le", - BOM::Utf32Be => "utf32_be", - } - } - - /// `removeAndConvertToUTF8AndFree` — if a re-encode is needed, free the input - /// and the caller replaces it with the new return. - pub fn remove_and_convert_to_utf8_and_free(self, mut bytes: Vec) -> Vec { - match self { - BOM::Utf8 => { - let n = Self::UTF8_BYTES.len(); - bytes.copy_within(n.., 0); - bytes.truncate(bytes.len() - n); - bytes - } - BOM::Utf16Le => { - // `trimmed` is `&[u8]` at offset 2 of a `Vec` allocation; its - // alignment is not guaranteed ≥ 2, so reinterpreting it as `&[u16]` - // is UB. Route through the - // byte-level helper which copies into an aligned `Vec` first. - let trimmed = &bytes[Self::UTF16_LE_BYTES.len()..]; - let out = strings::to_utf8_alloc_from_le_bytes(trimmed); - drop(bytes); - out - } - _ => { - // TODO: this needs to re-encode, for now we just remove the BOM - let n = self.header().len(); - bytes.copy_within(n.., 0); - bytes.truncate(bytes.len() - n); - bytes - } - } - } - - /// `removeAndConvertToUTF8WithoutDealloc` — required for `use_shared_buffer`. - /// We cannot free `list`'s pointer; the returned slice always points to - /// `list.as_ptr()`. `list` may be grown. - pub fn remove_and_convert_to_utf8_without_dealloc<'a>(self, list: &'a mut Vec) -> &'a [u8] { - match self { - BOM::Utf8 => { - let n = Self::UTF8_BYTES.len(); - let len = list.len(); - list.copy_within(n.., 0); - &list[..len - n] - } - BOM::Utf16Le => { - // See `remove_and_convert_to_utf8_and_free` — `&list[2..]` has no - // u16-alignment guarantee, so use the byte-level transcode helper. - let out = strings::to_utf8_alloc_from_le_bytes(&list[Self::UTF16_LE_BYTES.len()..]); - // `clear` keeps capacity (the "without_dealloc" contract); - // `extend_from_slice` grows only if needed — safe equivalent of - // the prior reserve/`set_len`/`copy_from_slice` open-coding. - list.clear(); - list.extend_from_slice(&out); - &list[..] - } - _ => { - // TODO: this needs to re-encode, for now we just remove the BOM - let n = self.header().len(); - let len = list.len(); - list.copy_within(n.., 0); - &list[..len - n] - } - } - } -} +pub use bun_core::immutable::BOM; pub(crate) mod preallocate { pub(crate) mod counts { @@ -1466,6 +1345,12 @@ impl ModKey { _: &[u8], file: &bun_sys::File, ) -> Result { + Self::from_file(file) + } + + /// `generate` minus the unread `&mut RealFS` / path args, for callers + /// (e.g. `linker::get_mod_key`) that don't hold a `RealFS`. + pub fn from_file(file: &bun_sys::File) -> Result { let stat = file.stat()?; const NS_PER_S: i128 = 1_000_000_000; @@ -1900,9 +1785,9 @@ impl RealFS { // `readFileError` (a no-op in release). // • `RealFS::read_file_with_handle_and_allocator` — keeps the existing // fs.rs:2121 signature for spec-shape fidelity; thin forward. -// • `read_file_contents` — runtime-bool → const-generic dispatcher for the -// two `cache::Fs` callers (resolver/lib.rs + bundler/cache.rs), which take -// `use_shared_buffer`/`stream` at runtime and want only the bytes. +// • `read_file_contents` — runtime-bool → const-generic dispatcher for +// `cache::Fs` (resolver/lib.rs; re-exported by bundler/cache.rs), which +// takes `use_shared_buffer`/`stream` at runtime and wants only the bytes. // ══════════════════════════════════════════════════════════════════════════ /// Runtime-bool → const-generic dispatcher for `cache::Fs::read_file{,_shared}`. @@ -2267,183 +2152,210 @@ impl RealFS { existing_fd: Fd, store_fd: bool, ) -> Result { - #[cfg(windows)] - let _ = (existing_fd, store_fd); - let mut cache = EntryCache { - kind: EntryKind::File, - symlink: Interned::EMPTY, - fd: Fd::INVALID, - }; + kind_impl( + self.cwd, + dir_, + base, + existing_fd, + store_fd, + || self.need_to_close_files(), + FileSystem::set_max_fd, + |s| Ok(FilenameStore::instance().append(s)?), + ) + } - let dir = dir_; - let combo: [&[u8]; 2] = [dir, base]; - let mut outpath = PathBuffer::uninit(); - let entry_path = - path_handler::join_abs_string_buf::(self.cwd, &mut outpath[..], &combo); - let entry_path_len = entry_path.len(); + // // Stores the file entries for directories we've listed before + // entries_mutex: std.Mutex + // entries map[string]entriesOrErr - outpath[entry_path_len + 1] = 0; - outpath[entry_path_len] = 0; + // // If true, do not use the "entries" cache + // doNotCacheEntries bool +} - let absolute_path_c = ZStr::from_buf(&outpath[..], entry_path_len); +/// Canonical `RealFS.kind` body (lstat + symlink-follow + realpath), shared by +/// this `RealFS` and the inline `fs::RealFS` in lib.rs until the +/// RealFS/Implementation dedup lands. The closures carry the per-copy +/// dependencies: the fd-budget check, the max-fd watermark, and the +/// symlink-target interning store. +pub(crate) fn kind_impl( + cwd: &[u8], + dir_: &[u8], + base: &[u8], + existing_fd: Fd, + store_fd: bool, + need_to_close_files: impl FnOnce() -> bool, + set_max_fd: impl FnOnce(bun_sys::RawFd), + intern_symlink: impl FnOnce(&[u8]) -> Result<&'static [u8], bun_core::Error>, +) -> Result { + #[cfg(windows)] + let _ = (existing_fd, store_fd, need_to_close_files, set_max_fd); + let mut cache = EntryCache { + kind: EntryKind::File, + symlink: Interned::EMPTY, + fd: Fd::INVALID, + }; - #[cfg(windows)] - { - let file = bun_sys::get_file_attributes(absolute_path_c) - .ok_or(bun_core::err!("FileNotFound"))?; - // A Windows reparse point carries FILE_ATTRIBUTE_DIRECTORY iff - // the link is a directory link (junctions always do; symlinks - // do iff created with SYMBOLIC_LINK_FLAG_DIRECTORY; AppExec - // links and file symlinks don't), so this is already the - // correct `Entry.Kind` without following the chain. - cache.kind = if file.is_directory { + let dir = dir_; + let combo: [&[u8]; 2] = [dir, base]; + let mut outpath = PathBuffer::uninit(); + let entry_path = + path_handler::join_abs_string_buf::(cwd, &mut outpath[..], &combo); + let entry_path_len = entry_path.len(); + + outpath[entry_path_len + 1] = 0; + outpath[entry_path_len] = 0; + + let absolute_path_c = ZStr::from_buf(&outpath[..], entry_path_len); + + #[cfg(windows)] + { + let file = + bun_sys::get_file_attributes(absolute_path_c).ok_or(bun_core::err!("FileNotFound"))?; + // A Windows reparse point carries FILE_ATTRIBUTE_DIRECTORY iff + // the link is a directory link (junctions always do; symlinks + // do iff created with SYMBOLIC_LINK_FLAG_DIRECTORY; AppExec + // links and file symlinks don't), so this is already the + // correct `Entry.Kind` without following the chain. + cache.kind = if file.is_directory { + EntryKind::Dir + } else { + EntryKind::File + }; + if !file.is_reparse_point { + return Ok(cache); + } + + // For the realpath, open the path and let the kernel follow + // every hop, then `GetFinalPathNameByHandle` (same as libuv's + // `uv_fs_realpath`). The previous manual readlink+join loop + // resolved relative targets against `dirname(absolute_path_c)`, + // but that path may itself contain unresolved intermediate + // symlinks (e.g. with the isolated linker's global virtual + // store, `node_modules/.bun/` is a symlink into + // `/links/`, and the dep symlinks inside point at + // siblings via `..\..\-`). Windows resolves + // relative reparse targets against the *real* parent, so the + // join landed in the project-side `.bun/` instead of + // `/links/`, the re-stat returned FileNotFound, the + // error was swallowed at `Entry.kind`, and a directory symlink + // was permanently misclassified as `.file` — surfacing as + // EISDIR at module load time. + use bun_sys::windows as w; + let mut wbuf = bun_paths::w_path_buffer_pool::get(); + let wpath = strings::paths::to_kernel32_path(&mut *wbuf, absolute_path_c.as_bytes()); + // SAFETY: `wpath` is NUL-terminated WTF-16 backed by the pooled + // `WPathBuffer`; null SECURITY_ATTRIBUTES / template handle are + // documented-valid for `CreateFileW`. + let handle = unsafe { + w::kernel32::CreateFileW( + wpath.as_ptr(), + 0, + w::FILE_SHARE_READ | w::FILE_SHARE_WRITE | w::FILE_SHARE_DELETE, + core::ptr::null_mut(), + w::OPEN_EXISTING, + // FILE_FLAG_BACKUP_SEMANTICS lets us open directories; + // omitting FILE_FLAG_OPEN_REPARSE_POINT makes Windows + // follow the full reparse chain to the final target. + w::FILE_FLAG_BACKUP_SEMANTICS, + core::ptr::null_mut(), + ) + }; + // Dangling link / loop / EACCES: `cache.kind` is already set + // from the link's own directory bit, which is correct for all + // of those. `Entry.kind`/`Entry.symlink` swallow errors and + // fall back to the `.file` placeholder anyway, so returning + // the half-populated cache is strictly better than `try`. + // Empty `cache.symlink` makes the resolver fall back to + // `parent.abs_real_path + base`. + if handle == w::INVALID_HANDLE_VALUE { + return Ok(cache); + } + scopeguard::defer! { + // SAFETY: `handle` ≠ INVALID_HANDLE_VALUE (checked above). + let _ = unsafe { w::CloseHandle(handle) }; + } + + let mut info: w::BY_HANDLE_FILE_INFORMATION = + // SAFETY: all-zero is a valid BY_HANDLE_FILE_INFORMATION (POD) + unsafe { bun_core::ffi::zeroed_unchecked() }; + // SAFETY: `handle` is a valid file handle for the scope. + if unsafe { w::GetFileInformationByHandle(handle, &mut info) } != 0 { + cache.kind = if info.dwFileAttributes & w::FILE_ATTRIBUTE_DIRECTORY != 0 { EntryKind::Dir } else { EntryKind::File }; - if !file.is_reparse_point { - return Ok(cache); - } - - // For the realpath, open the path and let the kernel follow - // every hop, then `GetFinalPathNameByHandle` (same as libuv's - // `uv_fs_realpath`). The previous manual readlink+join loop - // resolved relative targets against `dirname(absolute_path_c)`, - // but that path may itself contain unresolved intermediate - // symlinks (e.g. with the isolated linker's global virtual - // store, `node_modules/.bun/` is a symlink into - // `/links/`, and the dep symlinks inside point at - // siblings via `..\..\-`). Windows resolves - // relative reparse targets against the *real* parent, so the - // join landed in the project-side `.bun/` instead of - // `/links/`, the re-stat returned FileNotFound, the - // error was swallowed at `Entry.kind`, and a directory symlink - // was permanently misclassified as `.file` — surfacing as - // EISDIR at module load time. - use bun_sys::windows as w; - let mut wbuf = bun_paths::w_path_buffer_pool::get(); - let wpath = strings::paths::to_kernel32_path(&mut *wbuf, absolute_path_c.as_bytes()); - // SAFETY: `wpath` is NUL-terminated WTF-16 backed by the pooled - // `WPathBuffer`; null SECURITY_ATTRIBUTES / template handle are - // documented-valid for `CreateFileW`. - let handle = unsafe { - w::kernel32::CreateFileW( - wpath.as_ptr(), - 0, - w::FILE_SHARE_READ | w::FILE_SHARE_WRITE | w::FILE_SHARE_DELETE, - core::ptr::null_mut(), - w::OPEN_EXISTING, - // FILE_FLAG_BACKUP_SEMANTICS lets us open directories; - // omitting FILE_FLAG_OPEN_REPARSE_POINT makes Windows - // follow the full reparse chain to the final target. - w::FILE_FLAG_BACKUP_SEMANTICS, - core::ptr::null_mut(), - ) - }; - // Dangling link / loop / EACCES: `cache.kind` is already set - // from the link's own directory bit, which is correct for all - // of those. `Entry.kind`/`Entry.symlink` swallow errors and - // fall back to the `.file` placeholder anyway, so returning - // the half-populated cache is strictly better than `try`. - // Empty `cache.symlink` makes the resolver fall back to - // `parent.abs_real_path + base`. - if handle == w::INVALID_HANDLE_VALUE { - return Ok(cache); - } - scopeguard::defer! { - // SAFETY: `handle` ≠ INVALID_HANDLE_VALUE (checked above). - let _ = unsafe { w::CloseHandle(handle) }; - } - - let mut info: w::BY_HANDLE_FILE_INFORMATION = - // SAFETY: all-zero is a valid BY_HANDLE_FILE_INFORMATION (POD) - unsafe { bun_core::ffi::zeroed_unchecked() }; - // SAFETY: `handle` is a valid file handle for the scope. - if unsafe { w::GetFileInformationByHandle(handle, &mut info) } != 0 { - cache.kind = if info.dwFileAttributes & w::FILE_ATTRIBUTE_DIRECTORY != 0 { - EntryKind::Dir - } else { - EntryKind::File - }; - } + } - let mut buf2 = bun_paths::path_buffer_pool::get(); - // `Fd` packs the kernel handle into its `u64` backing on Windows; - // round-trip via `usize` (HANDLE is pointer-sized). - match bun_sys::get_fd_path(Fd::from_native(handle as usize as u64), &mut *buf2) { - bun_sys::Result::Ok(real) => { - cache.symlink = Interned::from_static(FilenameStore::instance().append(real)?); - } - bun_sys::Result::Err(_) => {} + let mut buf2 = bun_paths::path_buffer_pool::get(); + // `Fd` packs the kernel handle into its `u64` backing on Windows; + // round-trip via `usize` (HANDLE is pointer-sized). + match bun_sys::get_fd_path(Fd::from_native(handle as usize as u64), &mut *buf2) { + bun_sys::Result::Ok(real) => { + cache.symlink = Interned::from_static(intern_symlink(real)?); } - return Ok(cache); + bun_sys::Result::Err(_) => {} } + return Ok(cache); + } - #[cfg(not(windows))] - { - let stat = bun_sys::lstat(absolute_path_c)?; - let mut file_kind = bun_sys::kind_from_mode(stat.st_mode as bun_sys::Mode); - let is_symlink = file_kind == bun_sys::FileKind::SymLink; - - let mut symlink: &[u8] = b""; - - if is_symlink { - let file: Fd = if let Some(valid) = existing_fd.unwrap_valid() { - valid - } else if store_fd { - bun_sys::open_file_absolute_z(absolute_path_c, bun_sys::OpenFlags::READ_ONLY)? - .into_raw() - } else { - // O_PATH is - // Linux-only; macOS/BSD use O_RDONLY. Both add O_NOCTTY|O_CLOEXEC. - #[cfg(any(target_os = "linux", target_os = "android"))] - let flags = bun_sys::O::PATH | bun_sys::O::CLOEXEC | bun_sys::O::NOCTTY; - #[cfg(not(any(target_os = "linux", target_os = "android")))] - let flags = bun_sys::O::RDONLY | bun_sys::O::CLOEXEC | bun_sys::O::NOCTTY; - bun_sys::open(absolute_path_c, flags, 0)? - }; - FileSystem::set_max_fd(file.native()); - - // close-or-store must run on - // BOTH success and error paths — use scopeguard so it happens even if - // stat()/get_fd_path() return early with `?`. - let need_to_close_files = self.need_to_close_files(); - let cache_ptr: *mut EntryCache = &raw mut cache; - let _guard = scopeguard::guard(file, move |file| { - if (!store_fd || need_to_close_files) && !existing_fd.is_valid() { - let _ = bun_sys::close(file); - } else if FeatureFlags::STORE_FILE_DESCRIPTORS { - // SAFETY: `cache_ptr` points into a stack local that outlives this guard. - unsafe { (*cache_ptr).fd = file }; - } - }); - - let file_stat = bun_sys::fstat(*_guard)?; - symlink = bun_sys::get_fd_path(*_guard, &mut outpath)?; - file_kind = bun_sys::kind_from_mode(file_stat.st_mode as bun_sys::Mode); - } + #[cfg(not(windows))] + { + let stat = bun_sys::lstat(absolute_path_c)?; + let mut file_kind = bun_sys::kind_from_mode(stat.st_mode as bun_sys::Mode); + let is_symlink = file_kind == bun_sys::FileKind::SymLink; - debug_assert!(file_kind != bun_sys::FileKind::SymLink); + let mut symlink: &[u8] = b""; - if file_kind == bun_sys::FileKind::Directory { - cache.kind = EntryKind::Dir; + if is_symlink { + let file: Fd = if let Some(valid) = existing_fd.unwrap_valid() { + valid + } else if store_fd { + bun_sys::open_file_absolute_z(absolute_path_c, bun_sys::OpenFlags::READ_ONLY)? + .into_raw() } else { - cache.kind = EntryKind::File; - } - if !symlink.is_empty() { - cache.symlink = Interned::from_static(FilenameStore::instance().append(symlink)?); - } + // O_PATH is + // Linux-only; macOS/BSD use O_RDONLY. Both add O_NOCTTY|O_CLOEXEC. + #[cfg(any(target_os = "linux", target_os = "android"))] + let flags = bun_sys::O::PATH | bun_sys::O::CLOEXEC | bun_sys::O::NOCTTY; + #[cfg(not(any(target_os = "linux", target_os = "android")))] + let flags = bun_sys::O::RDONLY | bun_sys::O::CLOEXEC | bun_sys::O::NOCTTY; + bun_sys::open(absolute_path_c, flags, 0)? + }; + set_max_fd(file.native()); + + // close-or-store must run on + // BOTH success and error paths — use scopeguard so it happens even if + // stat()/get_fd_path() return early with `?`. + let need_to_close_files = need_to_close_files(); + let cache_ptr: *mut EntryCache = &raw mut cache; + let _guard = scopeguard::guard(file, move |file| { + if (!store_fd || need_to_close_files) && !existing_fd.is_valid() { + let _ = bun_sys::close(file); + } else if FeatureFlags::STORE_FILE_DESCRIPTORS { + // SAFETY: `cache_ptr` points into a stack local that outlives this guard. + unsafe { (*cache_ptr).fd = file }; + } + }); - Ok(cache) + let file_stat = bun_sys::fstat(*_guard)?; + symlink = bun_sys::get_fd_path(*_guard, &mut outpath)?; + file_kind = bun_sys::kind_from_mode(file_stat.st_mode as bun_sys::Mode); } - } - // // Stores the file entries for directories we've listed before - // entries_mutex: std.Mutex - // entries map[string]entriesOrErr + debug_assert!(file_kind != bun_sys::FileKind::SymLink); - // // If true, do not use the "entries" cache - // doNotCacheEntries bool + if file_kind == bun_sys::FileKind::Directory { + cache.kind = EntryKind::Dir; + } else { + cache.kind = EntryKind::File; + } + if !symlink.is_empty() { + cache.symlink = Interned::from_static(intern_symlink(symlink)?); + } + + Ok(cache) + } } impl EntryKindResolver for RealFS { diff --git a/src/resolver/lib.rs b/src/resolver/lib.rs index cb2c145c37a2..3e94c326d904 100644 --- a/src/resolver/lib.rs +++ b/src/resolver/lib.rs @@ -10,12 +10,13 @@ // crate::StandaloneModuleGraph trait; HardcodedModule -> bun_resolve_builtins. // ────────────────────────────────────────────────────────────────────────── -// Submodules. `fs.rs` (full RealFS readdir/stat/kind path) is now un-gated as +// Submodules. `fs.rs` (full RealFS readdir/stat/kind path) is mounted as // `fs_full`; the inline `pub mod fs` below remains the canonical type surface -// (FileSystem, RealFS, Path, PathName, Entry, DirEntry, EntryLookup, -// EntriesOption, Implementation) until the body switches to `fs_full::*` -// wholesale. `fs_full` compiles to validate the port and is link-dead until -// re-exported. +// (FileSystem, RealFS, Path, PathName, EntriesOption, Implementation) until +// the body switches to `fs_full::*` wholesale. Entry/DirEntry, the read-file +// helpers, `ModKey`, and the shared `kind_impl`/`adjust_ulimit`/temp-dir +// bodies already live in `fs_full` and are re-exported or delegated to from +// the inline module. pub mod data_url; pub mod dir_info; #[path = "fs.rs"] @@ -760,7 +761,6 @@ pub mod fs { use bun_core::Generation; use bun_paths::strings; - use bun_ptr::Interned; use bun_sys::Fd; use bun_threading::Mutex; @@ -1081,40 +1081,10 @@ pub mod fs { } /// Port of `RealFS.adjustUlimit` — always try to max out how many - /// files we can keep open. + /// files we can keep open. Canonical body: `fs_full::RealFS`. + #[inline] pub fn adjust_ulimit() -> core::result::Result { - #[cfg(not(unix))] - { - Ok(usize::MAX) - } - #[cfg(unix)] - { - let resource = bun_sys::posix::RlimitResource::NOFILE; - let mut lim = bun_sys::posix::getrlimit(resource)?; - - // Cap at 1<<20 to match Node.js. On macOS the hard limit defaults to - // RLIM_INFINITY; raising soft anywhere near INT_MAX breaks child processes - // that read the limit into an int. - let target = { - // musl has extremely low defaults, so ensure at least 163840 there. - #[cfg(target_env = "musl")] - let max = lim.max.max(163_840); - #[cfg(not(target_env = "musl"))] - let max = lim.max; - max.min(1 << 20) - }; - if lim.cur < target { - let mut raised = lim; - raised.cur = target; - // Don't lower the hard limit (Node only touches rlim_cur). The @max - // is for the musl branch above, which may raise past the current hard. - raised.max = lim.max.max(target); - if bun_sys::posix::setrlimit(resource, raised).is_ok() { - lim.cur = raised.cur; - } - } - Ok(usize::try_from(lim.cur).expect("int cast")) - } + crate::fs_full::RealFS::adjust_ulimit() } /// `open(path, O_DIRECTORY)`. @@ -1394,7 +1364,8 @@ pub mod fs { /// lstat + (if symlink) open + fstat + /// readlink to populate an `EntryCache`. Windows: `GetFileAttributesW` + /// (if reparse point) `CreateFileW`-follow + `GetFinalPathNameByHandle` - /// realpath. + /// realpath. Canonical body: `fs_full::kind_impl`; the closures keep + /// this copy's max-fd watermark and `FilenameStore` in use. pub fn kind( &mut self, dir_: &[u8], @@ -1402,175 +1373,16 @@ pub mod fs { existing_fd: Fd, store_fd: bool, ) -> core::result::Result { - use bun_paths::resolve_path::{join_abs_string_buf, platform}; - #[cfg(not(windows))] - use bun_sys::{FileKind, kind_from_mode}; - - let mut cache = EntryCache { - kind: EntryKind::File, - symlink: Interned::EMPTY, - fd: Fd::INVALID, - }; - - let combo: [&[u8]; 2] = [dir_, base]; - let mut outpath = bun_paths::PathBuffer::uninit(); - let entry_path_len = - join_abs_string_buf::(self.cwd, &mut outpath[..], &combo).len(); - - outpath[entry_path_len + 1] = 0; - outpath[entry_path_len] = 0; - let absolute_path_c = ZStr::from_buf(&outpath[..], entry_path_len); - - #[cfg(windows)] - { - use bun_sys::windows as w; - let _ = (existing_fd, store_fd); - let file = bun_sys::get_file_attributes(absolute_path_c) - .ok_or(bun_core::err!("FileNotFound"))?; - // A Windows reparse point carries FILE_ATTRIBUTE_DIRECTORY iff - // the link is a directory link (junctions always do; symlinks - // do iff created with SYMBOLIC_LINK_FLAG_DIRECTORY; AppExec - // links and file symlinks don't), so this is already the - // correct `Entry.Kind` without following the chain. - cache.kind = if file.is_directory { - EntryKind::Dir - } else { - EntryKind::File - }; - if !file.is_reparse_point { - return Ok(cache); - } - - // For the realpath, open the path and let the kernel follow - // every hop, then `GetFinalPathNameByHandle` (same as libuv's - // `uv_fs_realpath`). The previous manual readlink+join loop - // resolved relative targets against `dirname(absolute_path_c)`, - // but that path may itself contain unresolved intermediate - // symlinks (e.g. with the isolated linker's global virtual - // store, `node_modules/.bun/` is a symlink into - // `/links/`, and the dep symlinks inside point at - // siblings via `..\..\-`). Windows resolves - // relative reparse targets against the *real* parent, so the - // join landed in the project-side `.bun/` instead of - // `/links/`, the re-stat returned FileNotFound, the - // error was swallowed at `Entry.kind`, and a directory symlink - // was permanently misclassified as `.file` — surfacing as - // EISDIR at module load time. - let mut wbuf = bun_paths::w_path_buffer_pool::get(); - let wpath = bun_paths::strings::paths::to_kernel32_path( - &mut wbuf.0[..], - absolute_path_c.as_bytes(), - ); - // SAFETY: `wpath` is NUL-terminated UTF-16; null security/template handles. - let handle = unsafe { - w::CreateFileW( - wpath.as_ptr(), - 0, - w::FILE_SHARE_READ | w::FILE_SHARE_WRITE | w::FILE_SHARE_DELETE, - core::ptr::null_mut(), - w::OPEN_EXISTING, - // FILE_FLAG_BACKUP_SEMANTICS lets us open directories; - // omitting FILE_FLAG_OPEN_REPARSE_POINT makes Windows - // follow the full reparse chain to the final target. - w::FILE_FLAG_BACKUP_SEMANTICS, - core::ptr::null_mut(), - ) - }; - // Dangling link / loop / EACCES: `cache.kind` is already set - // from the link's own directory bit, which is correct for all - // of those. `Entry.kind`/`Entry.symlink` swallow errors and - // fall back to the `.file` placeholder anyway, so returning - // the half-populated cache is strictly better than `try`. - // Empty `cache.symlink` makes the resolver fall back to - // `parent.abs_real_path + base`. - if handle == w::INVALID_HANDLE_VALUE { - return Ok(cache); - } - scopeguard::defer! { - // SAFETY: `handle` is a valid HANDLE from CreateFileW above. - unsafe { let _ = w::CloseHandle(handle); } - } - - let mut info: w::BY_HANDLE_FILE_INFORMATION = bun_core::ffi::zeroed(); - // SAFETY: `handle` is valid; `info` is a valid out-param. - if unsafe { w::GetFileInformationByHandle(handle, &mut info) } != 0 { - cache.kind = if info.dwFileAttributes & w::FILE_ATTRIBUTE_DIRECTORY != 0 { - EntryKind::Dir - } else { - EntryKind::File - }; - } - - let mut buf2 = bun_paths::path_buffer_pool::get(); - if let Ok(real) = bun_sys::get_fd_path(Fd::from_system(handle), &mut buf2) { - cache.symlink = - Interned::from_static(FilenameStore::instance().append_slice(real)?); - } - return Ok(cache); - } - - #[cfg(not(windows))] - { - let stat_ = bun_sys::lstat(absolute_path_c)?; - let is_symlink = - kind_from_mode(stat_.st_mode as bun_sys::Mode) == FileKind::SymLink; - let mut file_kind = kind_from_mode(stat_.st_mode as bun_sys::Mode); - - let mut symlink: &[u8] = b""; - - if is_symlink { - let file: Fd = if let Some(valid) = existing_fd.unwrap_valid() { - valid - } else if store_fd { - bun_sys::open_file_absolute_z( - absolute_path_c, - bun_sys::OpenFlags::READ_ONLY, - )? - .into_raw() - } else { - // O_PATH is - // Linux-only; macOS/BSD use O_RDONLY. Both add O_NOCTTY|O_CLOEXEC. - #[cfg(any(target_os = "linux", target_os = "android"))] - let flags = bun_sys::O::PATH | bun_sys::O::CLOEXEC | bun_sys::O::NOCTTY; - #[cfg(not(any(target_os = "linux", target_os = "android")))] - let flags = bun_sys::O::RDONLY | bun_sys::O::CLOEXEC | bun_sys::O::NOCTTY; - bun_sys::open(absolute_path_c, flags, 0)? - }; - FileSystem::set_max_fd(file.native()); - - // The close-or-store cleanup runs on - // BOTH success and error paths — use scopeguard so close-or-store happens even if - // fstat()/get_fd_path() return early with `?`. - let need_to_close_files = self.need_to_close_files(); - let cache_ptr: *mut EntryCache = &raw mut cache; - let _guard = scopeguard::guard(file, move |file| { - if (!store_fd || need_to_close_files) && !existing_fd.is_valid() { - let _ = bun_sys::close(file); - } else if bun_core::feature_flags::STORE_FILE_DESCRIPTORS { - // SAFETY: `cache_ptr` points into a stack local that outlives this guard. - unsafe { (*cache_ptr).fd = file }; - } - }); - - let file_stat = bun_sys::fstat(*_guard)?; - symlink = bun_sys::get_fd_path(*_guard, &mut outpath)?; - file_kind = kind_from_mode(file_stat.st_mode as bun_sys::Mode); - } - - debug_assert!(file_kind != FileKind::SymLink); - - cache.kind = if file_kind == FileKind::Directory { - EntryKind::Dir - } else { - EntryKind::File - }; - if !symlink.is_empty() { - cache.symlink = - Interned::from_static(FilenameStore::instance().append_slice(symlink)?); - } - - Ok(cache) - } + crate::fs_full::kind_impl( + self.cwd, + dir_, + base, + existing_fd, + store_fd, + || self.need_to_close_files(), + FileSystem::set_max_fd, + |s| FilenameStore::instance().append_slice(s), + ) } } @@ -1668,91 +1480,22 @@ pub mod fs { Some(unsafe { &mut *result_ptr }) } - fn platform_temp_dir_compute() -> &'static [u8] { - use bun_core::env_var; - // Try TMPDIR, TMP, and TEMP in that order, matching Node.js. - // https://github.com/nodejs/node/blob/e172be269890702bf2ad06252f2f152e7604d76c/src/node_credentials.cc#L132 - if let Some(dir) = env_var::TMPDIR - .get_not_empty() - .or_else(|| env_var::TMP.get_not_empty()) - .or_else(|| env_var::TEMP.get_not_empty()) - { - if dir.len() > 1 && dir[dir.len() - 1] == bun_paths::SEP { - return &dir[0..dir.len() - 1]; - } - return dir; - } - - #[cfg(target_os = "windows")] - { - // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw#remarks - // The computed path borrows env-var storage joined with a literal, - // so it must own its buffer. This runs once for the process via - // `bun_core::Once` in `platform_temp_dir()`; the `OnceLock` here is - // the allowed process-lifetime singleton (PORTING.md §Forbidden - // exception), not a per-call leak. - static OWNED: std::sync::OnceLock> = std::sync::OnceLock::new(); - return OWNED - .get_or_init(|| { - if let Some(windir) = - env_var::SYSTEMROOT.get().or_else(|| env_var::WINDIR.get()) - { - let mut out = - bun_core::strings::without_trailing_slash(windir).to_vec(); - out.extend_from_slice(b"\\Temp"); - return out; - } - if let Some(profile) = env_var::HOME.get() { - let mut buf = bun_paths::PathBuffer::uninit(); - let parts: [&[u8]; 1] = [b"AppData\\Local\\Temp"]; - let out = bun_paths::resolve_path::join_abs_string_buf::< - bun_paths::resolve_path::platform::Loose, - >(profile, &mut buf[..], &parts); - return out.to_vec(); - } - let mut tmp_buf = bun_paths::PathBuffer::uninit(); - let cwd = match bun_sys::getcwd(&mut tmp_buf[..]) { - Ok(len) => &tmp_buf[..len], - Err(_) => panic!("Failed to get cwd for platformTempDir"), - }; - let root = bun_paths::resolve_path::windows_filesystem_root(cwd); - let mut out = bun_core::strings::without_trailing_slash(root).to_vec(); - out.extend_from_slice(b"\\Windows\\Temp"); - out - }) - .as_slice(); - } - #[cfg(target_os = "macos")] - { - return b"/private/tmp"; - } - #[cfg(target_os = "android")] - { - return b"/data/local/tmp"; - } - #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "android")))] - { - b"/tmp" - } - } - /// Platform temp directory, computed once per process. + /// Canonical body: `fs_full::RealFS`. + #[inline] pub fn platform_temp_dir() -> &'static [u8] { - static ONCE: bun_core::Once<&'static [u8]> = bun_core::Once::new(); - ONCE.call(Self::platform_temp_dir_compute) + crate::fs_full::RealFS::platform_temp_dir() } /// Non-empty `BUN_TMPDIR`, falling back to `platform_temp_dir`. + #[inline] pub fn tmpdir_path() -> &'static [u8] { - bun_core::env_var::BUN_TMPDIR - .get_not_empty() - .unwrap_or_else(Self::platform_temp_dir) + crate::fs_full::RealFS::tmpdir_path() } + #[inline] pub fn get_default_temp_dir() -> &'static [u8] { - bun_core::env_var::BUN_TMPDIR - .get() - .unwrap_or_else(Self::platform_temp_dir) + crate::fs_full::RealFS::get_default_temp_dir() } } @@ -1774,54 +1517,9 @@ pub mod fs { pub use super::fs_full::stat_hash::StatHash; /// Re-export `ModKey` from the full `fs.rs` port so `linker::get_mod_key` - /// can hash files without depending on `fs_full::RealFS` (a distinct type - /// from this inline `RealFS`). + /// can hash files via `ModKey::from_file` without depending on + /// `fs_full::RealFS` (a distinct type from this inline `RealFS`). pub use super::fs_full::ModKey; - impl ModKey { - /// RealFS-agnostic constructor. `fs_full::ModKey::generate`'s - /// `&mut RealFS` / `path` args are unread (fs.rs:1386); callers - /// reaching `ModKey` via this re-export hold the inline-`fs` `RealFS`, - /// which is a different type, so they need an entry point that doesn't - /// require `fs_full::RealFS`. Body is the spec `generate` minus the - /// dead args. - pub fn from_file(file: &bun_sys::File) -> core::result::Result { - let stat = file.stat()?; - - const NS_PER_S: i128 = 1_000_000_000; - // `bun_sys::Stat` is `libc::stat`. - // Reconstruct `mtime` (i128 ns) from `st_mtime` (sec) + - // `st_mtime_nsec` (ns). The `libc` crate flattens BSD/Darwin - // `st_mtimespec` into `st_mtime`/`st_mtime_nsec`, so the access is - // uniform on all `unix`. - #[cfg(unix)] - let mtime: i128 = (stat.st_mtime as i128) * NS_PER_S + stat.st_mtime_nsec as i128; - #[cfg(windows)] - let mtime: i128 = (stat.mtim.sec as i128) * NS_PER_S + stat.mtim.nsec as i128; - let seconds = mtime / NS_PER_S; - - // We can't detect changes if the file system zeros out the - // modification time - if seconds == 0 && NS_PER_S == 0 { - return Err(bun_core::err!("Unusable")); - } - - // Don't generate a modification key if the file is too new - let now = bun_core::time::nano_timestamp(); - let now_seconds = now / NS_PER_S; - // `seconds > seconds` is always false — intentionally kept - #[allow(clippy::eq_op)] - if seconds > seconds || (seconds == now_seconds && mtime > now) { - return Err(bun_core::err!("Unusable")); - } - - Ok(ModKey { - inode: stat.st_ino, - size: stat.st_size as u64, - mtime, - mode: stat.st_mode as bun_sys::Mode, - }) - } - } pub mod file_system { pub use super::{DirEntry, DirnameStore, Entry, EntryKind, FilenameStore, RealFS}; diff --git a/src/resolver/resolver.rs b/src/resolver/resolver.rs index 1deea6ede33c..95a11605c695 100644 --- a/src/resolver/resolver.rs +++ b/src/resolver/resolver.rs @@ -2780,116 +2780,18 @@ impl<'a> Resolver<'a> { if let Some(package_json) = pkg_dir_info.package_json() { if let Some(exports_map) = package_json.exports.as_ref() { - // The condition set is determined by the kind of import let mut module_type = package_json.module_type; - // NOTE: keeping a single - // `ESModule` (which holds `&mut self.debug_logs`) alive across a - // `&mut self` call is aliased-&mut UB. Build a fresh short-lived - // `ESModule` per `resolve` call so its borrow ends before - // `self.handle_esm_resolution` re-borrows `self`. - // Resolve against the path "/", then join it with the absolute - // directory path. This is done because ESM package resolution uses - // URLs while our path resolution uses file system paths. We don't - // want problems due to Windows paths, which are very unlike URL - // paths. We also want to avoid any "%" characters in the absolute - // directory path accidentally being interpreted as URL escapes. - { - let esm_resolution = ESModule { - conditions: match kind { - ast::ImportKind::Require - | ast::ImportKind::RequireResolve => { - &self.opts.conditions.require - } - ast::ImportKind::At - | ast::ImportKind::AtConditional => { - &self.opts.conditions.style - } - _ => &self.opts.conditions.import, - }, - debug_logs: self.debug_logs.as_mut(), - module_type: &mut module_type, - } - .resolve(b"/", esm.subpath, &exports_map.root); - // ESModule temporary dropped here; `self` is unborrowed. - - if self - .handle_esm_resolution( - esm_resolution, - abs_package_path, - kind, - package_json, - esm.subpath, - out, - ) - .is_success() - { - out.is_node_module = true; - out.module_type = module_type; - self.extension_order = prev_extension_order; - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchStatus::Success; - } - } - - // Some popular packages forget to include the extension in their - // exports map, so we try again without the extension. - // - // This is useful for browser-like environments - // where you want a file extension in the URL - // pathname by convention. Vite does this. - // - // React is an example of a package that doesn't include file extensions. - // { - // "exports": { - // ".": "./index.js", - // "./jsx-runtime": "./jsx-runtime.js", - // } - // } - // - // We limit this behavior just to ".js" files. - let extname = bun_paths::extension(esm.subpath); - if extname == b".js" && esm.subpath.len() > 3 { - let esm_resolution = ESModule { - conditions: match kind { - ast::ImportKind::Require - | ast::ImportKind::RequireResolve => { - &self.opts.conditions.require - } - ast::ImportKind::At - | ast::ImportKind::AtConditional => { - &self.opts.conditions.style - } - _ => &self.opts.conditions.import, - }, - debug_logs: self.debug_logs.as_mut(), - module_type: &mut module_type, - } - .resolve( - b"/", - &esm.subpath[0..esm.subpath.len() - 3], - &exports_map.root, - ); - if self - .handle_esm_resolution( - esm_resolution, - abs_package_path, - kind, - package_json, - esm.subpath, - out, - ) - .is_success() - { - out.is_node_module = true; - out.module_type = module_type; - self.extension_order = prev_extension_order; - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchStatus::Success; - } + if self.resolve_esm_exports( + kind, + esm.subpath, + &exports_map.root, + abs_package_path, + package_json, + &mut module_type, + prev_extension_order, + out, + ) { + return MatchStatus::Success; } // if they hid "package.json" from "exports", still allow importing it. @@ -3284,7 +3186,8 @@ impl<'a> Resolver<'a> { if let Some(package_json) = pkg_dir_info.package_json() { if let Some(exports_map) = package_json.exports.as_ref() { // The condition set is determined by the kind of import - // NOTE: reshaped for borrowck — see identical note above. + // NOTE: reshaped for borrowck — see the note on + // `resolve_esm_exports`. // Resolve against the path "/", then join it with the absolute // directory path. This is done because ESM package resolution uses // URLs while our path resolution uses file system paths. We don't @@ -3745,6 +3648,93 @@ impl<'a> Resolver<'a> { unreachable!("TODO: implement enqueueDependencyToResolve for non-root packages") } + /// Resolves `subpath` against a package's `exports` map, picking the + /// condition set for `kind`. On success, fills `out`, restores + /// `prev_extension_order`, and unindents the debug logs. + /// + /// Resolve against the path "/", then join it with the absolute directory + /// path: ESM package resolution uses URLs while our path resolution uses + /// file system paths, and we want neither Windows-path problems nor "%" + /// characters being interpreted as URL escapes. + /// + /// NOTE: keeping a single `ESModule` (which holds `&mut self.debug_logs`) + /// alive across a `&mut self` call is aliased-&mut UB, so it must drop + /// before `self.handle_esm_resolution` re-borrows `self`. + #[allow(clippy::too_many_arguments)] + fn resolve_esm_exports( + &mut self, + kind: ast::ImportKind, + subpath: &[u8], + exports_root: &crate::package_json::Entry, + abs_package_path: &[u8], + package_json: &PackageJSON, + module_type: &mut options::ModuleType, + prev_extension_order: options::ExtOrder, + out: &mut MatchResult, + ) -> bool { + let mut resolve_subpath = subpath; + loop { + let esm_resolution = ESModule { + conditions: match kind { + ast::ImportKind::Require | ast::ImportKind::RequireResolve => { + &self.opts.conditions.require + } + ast::ImportKind::At | ast::ImportKind::AtConditional => { + &self.opts.conditions.style + } + _ => &self.opts.conditions.import, + }, + debug_logs: self.debug_logs.as_mut(), + module_type: &mut *module_type, + } + .resolve(b"/", resolve_subpath, exports_root); + + if self + .handle_esm_resolution( + esm_resolution, + abs_package_path, + kind, + package_json, + subpath, + out, + ) + .is_success() + { + out.is_node_module = true; + out.module_type = *module_type; + self.extension_order = prev_extension_order; + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return true; + } + + // Some popular packages forget to include the extension in their + // exports map, so we try again without the extension. + // + // This is useful for browser-like environments + // where you want a file extension in the URL + // pathname by convention. Vite does this. + // + // React is an example of a package that doesn't include file extensions. + // { + // "exports": { + // ".": "./index.js", + // "./jsx-runtime": "./jsx-runtime.js", + // } + // } + // + // We limit this behavior just to ".js" files. + if resolve_subpath.len() < subpath.len() + || bun_paths::extension(subpath) != b".js" + || subpath.len() <= 3 + { + return false; + } + resolve_subpath = &subpath[0..subpath.len() - 3]; + } + } + fn handle_esm_resolution( &mut self, esm_resolution_: crate::package_json::Resolution, diff --git a/src/router/Cargo.toml b/src/router/Cargo.toml index f4a43995a095..8c80a339c356 100644 --- a/src/router/Cargo.toml +++ b/src/router/Cargo.toml @@ -32,7 +32,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 743513c9ff9b..ba237ede7de3 100644 --- a/src/router/lib.rs +++ b/src/router/lib.rs @@ -1940,278 +1940,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) -> Result<(), bun_core::Error> { - self.handle_request_called = true; - Ok(()) - } - - fn handle_redirect(&mut self, _: &[u8]) -> Result<(), bun_core::Error> { - self.redirect_called = true; - Ok(()) - } - } - - struct JavaScriptHandler; - impl JavaScriptHandler { - fn enqueue( - _: &mut MockRequestContextType, - _: &mut MockServer, - _: &mut route_param::List<'_>, - ) -> Result<(), bun_core::Error> { - 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) -> Result<(), bun_core::Error> { - Ok(()) - } - } - - fn make_test(cwd_path: &[u8], data: &[(&str, &str)]) -> Result<(), bun_core::Error> { - 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)], - ) -> 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(|_| bun_core::err!("OutOfMemory"))?; - - // 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(|| bun_core::err!("FileNotFound"))?; - - // 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)], - ) -> Result, bun_core::Error> { - 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(|_| bun_core::err!("OutOfMemory"))?; - - // 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(|| bun_core::err!("FileNotFound"))?; - - // 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 e70d3c615c0b..1caaac5b3e0c 100644 --- a/src/runtime/api/HashObject.rs +++ b/src/runtime/api/HashObject.rs @@ -276,20 +276,7 @@ fn hash_wrap(global: &JSGlobalObject, frame: &CallFrame) -> Js input = blob.shared_view(); } else { match arg.js_type_loose() { - jsc::JSType::ArrayBuffer - | jsc::JSType::Int8Array - | jsc::JSType::Uint8Array - | jsc::JSType::Uint8ClampedArray - | jsc::JSType::Int16Array - | jsc::JSType::Uint16Array - | jsc::JSType::Int32Array - | jsc::JSType::Uint32Array - | jsc::JSType::Float16Array - | jsc::JSType::Float32Array - | jsc::JSType::Float64Array - | jsc::JSType::BigInt64Array - | jsc::JSType::BigUint64Array - | jsc::JSType::DataView => { + t if t.is_array_buffer_like() => { array_buffer = match arg.as_array_buffer(global) { Some(ab) => ab, None => { diff --git a/src/runtime/api/JSON5Object.rs b/src/runtime/api/JSON5Object.rs index 0ca08293d490..95ccbc895ba5 100644 --- a/src/runtime/api/JSON5Object.rs +++ b/src/runtime/api/JSON5Object.rs @@ -1,10 +1,9 @@ -use bun_ast::{E, Expr, expr::Data as ExprData}; +use bun_ast::ToJSError; use bun_collections::HashMap; -use bun_collections::VecExt; use bun_core::StackCheck; -use bun_core::{String as BunString, ZigString}; +use bun_core::String as BunString; use bun_js_parser::lexer; -use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsError, JsResult, StringJsc, wtf}; +use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsError, JsResult, wtf}; use bun_parsers::json5; pub(crate) fn create(global: &JSGlobalObject) -> JSValue { @@ -76,7 +75,7 @@ pub fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { } }; - expr_to_js(root, global) + expr_to_js(&root, global) }, ) } @@ -432,55 +431,15 @@ impl Stringifier { } } -fn estring_to_js(str: &E::EString, global: &JSGlobalObject) -> JsResult { - // NOTE: the JSON5 parser never builds ropes, so the simple slice → JS - // path is sufficient. - if str.is_utf16 { - let zig = ZigString::init_utf16(str.slice16()); - let bun_s = BunString::init(zig); - bun_s.to_js(global) - } else { - jsc::bun_string_jsc::create_utf8_for_js(global, str.slice8()) - } -} - -fn expr_to_js(expr: Expr, global: &JSGlobalObject) -> JsResult { - expr_to_js_with_check(expr, global, StackCheck::init()) -} - -fn expr_to_js_with_check( - expr: Expr, - global: &JSGlobalObject, - stack_check: StackCheck, -) -> JsResult { - if !stack_check.is_safe_to_recurse() { - return Err(global.throw_stack_overflow()); - } - match expr.data { - ExprData::ENull(_) => Ok(JSValue::NULL), - ExprData::EBoolean(boolean) => Ok(JSValue::from(boolean.value)), - ExprData::ENumber(number) => Ok(JSValue::js_number(number.value)), - ExprData::EString(str) => estring_to_js(str.get(), global), - ExprData::EArray(arr) => { - JSValue::create_array_from_iter(global, arr.slice().iter(), |item| { - expr_to_js_with_check(*item, global, stack_check) - }) - } - ExprData::EObject(obj) => { - let js_obj = JSValue::create_empty_object(global, obj.properties.len_u32() as usize); - for prop in obj.properties.slice() { - let key_expr = prop.key.expect("infallible: prop has key"); - let value = expr_to_js_with_check( - prop.value.expect("infallible: prop has value"), - global, - stack_check, - )?; - let key_js = expr_to_js_with_check(key_expr, global, stack_check)?; - let key_str = bun_core::OwnedString::new(key_js.to_bun_string(global)?); - js_obj.put_may_be_index(global, &key_str, value)?; - } - Ok(js_obj) - } - _ => Ok(JSValue::UNDEFINED), - } +fn expr_to_js(expr: &bun_ast::Expr, global: &JSGlobalObject) -> JsResult { + bun_js_parser_jsc::expr_to_js(expr, global).map_err(|err| match err { + ToJSError::OutOfMemory => JsError::OutOfMemory, + ToJSError::JSTerminated => JsError::Terminated, + // The exception (e.g. stack overflow) is already pending on the global. + ToJSError::JSError => JsError::Thrown, + // Unreachable: the JSON5 parser only emits literal nodes. + ToJSError::CannotConvertArgumentTypeToJS + | ToJSError::CannotConvertIdentifierToJS + | ToJSError::MacroError => global.throw(format_args!("Cannot convert JSON5 value to JS")), + }) } diff --git a/src/runtime/api/MarkdownObject.rs b/src/runtime/api/MarkdownObject.rs index a5e423cbdd5a..f58c9b3d04b6 100644 --- a/src/runtime/api/MarkdownObject.rs +++ b/src/runtime/api/MarkdownObject.rs @@ -57,6 +57,44 @@ impl Drop for PinnedView { } } +/// Validate the input argument and pin its backing buffer (if any). The +/// caller derives the byte slice via [`input_slice`] so the borrow of +/// `buffer`/`pinned` stays local to the caller's frame. +fn prepare_input( + global_this: &JSGlobalObject, + input_value: JSValue, +) -> JsResult<(StringOrBuffer, Option)> { + if input_value.is_empty_or_undefined_or_null() { + return Err(global_this + .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); + } + + let Some(buffer) = StringOrBuffer::from_js(global_this, input_value)? else { + return Err(global_this + .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); + }; + + let pinned = PinnedView::pin(global_this, &buffer)?; + Ok((buffer, pinned)) +} + +#[inline] +fn input_slice<'a>(buffer: &'a StringOrBuffer, pinned: &'a Option) -> &'a [u8] { + match pinned { + Some(p) => p.slice(), + None => buffer.slice(), + } +} + +fn map_parser_error(global_this: &JSGlobalObject, err: ParserError) -> bun_jsc::JsError { + match err { + ParserError::JSError => bun_jsc::JsError::Thrown, + ParserError::JSTerminated => bun_jsc::JsError::Terminated, + ParserError::OutOfMemory => global_this.throw_out_of_memory(), + ParserError::StackOverflow => global_this.throw_stack_overflow(), + } +} + pub(crate) fn create(global_this: &JSGlobalObject) -> JSValue { bun_jsc::create_host_function_object( global_this, @@ -77,21 +115,8 @@ pub(crate) fn create(global_this: &JSGlobalObject) -> JSValue { pub fn render_to_ansi(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { let [input_value, theme_value] = callframe.arguments_as_array::<2>(); - if input_value.is_empty_or_undefined_or_null() { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - } - - let Some(buffer) = StringOrBuffer::from_js(global_this, input_value)? else { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - }; - - let pinned = PinnedView::pin(global_this, &buffer)?; - let input: &[u8] = match &pinned { - Some(p) => p.slice(), - None => buffer.slice(), - }; + let (buffer, pinned) = prepare_input(global_this, input_value)?; + let input = input_slice(&buffer, &pinned); let mut theme = md::AnsiTheme { colors: true, @@ -150,21 +175,8 @@ pub(crate) fn render_to_html( ) -> JsResult { let [input_value, opts_value] = callframe.arguments_as_array::<2>(); - if input_value.is_empty_or_undefined_or_null() { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - } - - let Some(buffer) = StringOrBuffer::from_js(global_this, input_value)? else { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - }; - - let pinned = PinnedView::pin(global_this, &buffer)?; - let input: &[u8] = match &pinned { - Some(p) => p.slice(), - None => buffer.slice(), - }; + let (buffer, pinned) = prepare_input(global_this, input_value)?; + let input = input_slice(&buffer, &pinned); let options = parse_options(global_this, opts_value)?; @@ -253,21 +265,8 @@ fn parse_options(global_this: &JSGlobalObject, opts_value: JSValue) -> JsResult< pub(crate) fn render(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { let [input_value, callbacks_value, opts_value] = callframe.arguments_as_array::<3>(); - if input_value.is_empty_or_undefined_or_null() { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - } - - let Some(buffer) = StringOrBuffer::from_js(global_this, input_value)? else { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - }; - - let pinned = PinnedView::pin(global_this, &buffer)?; - let input: &[u8] = match &pinned { - Some(p) => p.slice(), - None => buffer.slice(), - }; + let (buffer, pinned) = prepare_input(global_this, input_value)?; + let input = input_slice(&buffer, &pinned); // Parse parser options from 3rd argument let options = parse_options(global_this, opts_value)?; @@ -286,14 +285,8 @@ pub(crate) fn render(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsR })?; // Run parser with the JS callback renderer - if let Err(err) = md::render_with_renderer(input, options, js_renderer.renderer()) { - return match err { - ParserError::JSError => Err(bun_jsc::JsError::Thrown), - ParserError::JSTerminated => Err(bun_jsc::JsError::Terminated), - ParserError::OutOfMemory => Err(global_this.throw_out_of_memory()), - ParserError::StackOverflow => Err(global_this.throw_stack_overflow()), - }; - } + md::render_with_renderer(input, options, js_renderer.renderer()) + .map_err(|err| map_parser_error(global_this, err))?; // Return accumulated result let result = js_renderer.get_result(); @@ -354,21 +347,8 @@ fn render_ast( ) -> JsResult { let [input_value, components_value, opts_value] = callframe.arguments_as_array::<3>(); - if input_value.is_empty_or_undefined_or_null() { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - } - - let Some(buffer) = StringOrBuffer::from_js(global_this, input_value)? else { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - }; - - let pinned = PinnedView::pin(global_this, &buffer)?; - let input: &[u8] = match &pinned { - Some(p) => p.slice(), - None => buffer.slice(), - }; + let (buffer, pinned) = prepare_input(global_this, input_value)?; + let input = input_slice(&buffer, &pinned); // Parse parser options from 3rd argument let options = parse_options(global_this, opts_value)?; @@ -391,14 +371,8 @@ fn render_ast( JSValue::UNDEFINED })?; - if let Err(err) = md::render_with_renderer(input, options, renderer.renderer()) { - return match err { - ParserError::JSError => Err(bun_jsc::JsError::Thrown), - ParserError::JSTerminated => Err(bun_jsc::JsError::Terminated), - ParserError::OutOfMemory => Err(global_this.throw_out_of_memory()), - ParserError::StackOverflow => Err(global_this.throw_stack_overflow()), - }; - } + md::render_with_renderer(input, options, renderer.renderer()) + .map_err(|err| map_parser_error(global_this, err))?; Ok(renderer.get_result()) } diff --git a/src/runtime/api/bun/h2_frame_parser.rs b/src/runtime/api/bun/h2_frame_parser.rs index 7e8b9c096c28..299d6d3672cc 100644 --- a/src/runtime/api/bun/h2_frame_parser.rs +++ b/src/runtime/api/bun/h2_frame_parser.rs @@ -18,6 +18,7 @@ use bun_collections::{ByteVecExt, HashMap as BunHashMap, HiveArrayFallback, VecE use bun_core::MutableString; use bun_core::String as BunString; use bun_http::lshpack; +use bun_http_types::h2::{is_lower_tchar, is_malformed_field_value}; use bun_jsc::AbortSignal; use bun_jsc::ErrorCode as JscErrorCode; use bun_jsc::StringJsc as _; @@ -603,11 +604,6 @@ fn is_valid_request_pseudo_header(name: &[u8]) -> bool { REQUEST_PSEUDO_HEADERS.contains(name) } -#[inline] -fn is_valid_header_value(value: &[u8]) -> bool { - !value.iter().any(|&c| matches!(c, 0 | b'\n' | b'\r')) -} - #[inline] fn is_malformed_field_name(name: &[u8]) -> bool { let rest = match name.split_first() { @@ -615,34 +611,7 @@ fn is_malformed_field_name(name: &[u8]) -> bool { Some((b':', rest)) => rest, Some(_) => name, }; - rest.is_empty() - || !rest.iter().all(|&c| { - matches!( - c, - b'a'..=b'z' - | b'0'..=b'9' - | b'!' - | b'#' - | b'$' - | b'%' - | b'&' - | b'\'' - | b'*' - | b'+' - | b'-' - | b'.' - | b'^' - | b'_' - | b'`' - | b'|' - | b'~' - ) - }) -} - -#[inline] -fn is_malformed_field_value(value: &[u8]) -> bool { - value.iter().any(|&c| c == 0 || c == b'\r' || c == b'\n') + rest.is_empty() || !rest.iter().all(|&c| is_lower_tchar(c)) } bun_core::comptime_string_set! { @@ -5356,30 +5325,47 @@ impl H2FrameParser { Ok(JSValue::UNDEFINED) } - #[bun_jsc::host_fn(method)] - pub(crate) fn get_end_after_headers( - this: &Self, + /// Shared prologue for host fns that take a stream id argument: validates + /// the JS value, optionally rejects id 0 / ids above `MAX_STREAM_ID`, and + /// resolves the live `Stream` pointer in `self.streams`. `not_number_msg` + /// preserves each call site's user-visible error for a non-number argument. + #[inline] + fn stream_from_js_arg( + &self, global_object: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - let args_list = callframe.arguments_old::<1>(); - if args_list.len < 1 { - return Err(global_object.throw(format_args!("Expected stream argument"))); - } - let stream_arg = args_list.ptr[0]; - + stream_arg: JSValue, + not_number_msg: &str, + ) -> JsResult<*mut Stream> { if !stream_arg.is_number() { - return Err(global_object.throw(format_args!("Invalid stream id"))); + return Err(global_object.throw(format_args!("{not_number_msg}"))); } let stream_id = stream_arg.to_u32(); - if stream_id == 0 { + if (CHECK_ZERO && stream_id == 0) || (CHECK_MAX && stream_id > MAX_STREAM_ID) { return Err(global_object.throw(format_args!("Invalid stream id"))); } - let Some(stream) = this.streams.get().get(&stream_id).copied() else { + let Some(stream) = self.streams.get().get(&stream_id).copied() else { return Err(global_object.throw(format_args!("Invalid stream id"))); }; + Ok(stream) + } + + #[bun_jsc::host_fn(method)] + pub(crate) fn get_end_after_headers( + this: &Self, + global_object: &JSGlobalObject, + callframe: &CallFrame, + ) -> JsResult { + let args_list = callframe.arguments_old::<1>(); + if args_list.len < 1 { + return Err(global_object.throw(format_args!("Expected stream argument"))); + } + let stream = this.stream_from_js_arg::( + global_object, + args_list.ptr[0], + "Invalid stream id", + )?; // SAFETY: stream is *mut Stream from self.streams; valid while the map entry exists Ok(JSValue::from(unsafe { (*stream).end_after_headers })) @@ -5395,20 +5381,11 @@ impl H2FrameParser { if args_list.len < 1 { return Err(global_object.throw(format_args!("Expected stream argument"))); } - let stream_arg = args_list.ptr[0]; - - if !stream_arg.is_number() { - return Err(global_object.throw(format_args!("Invalid stream id"))); - } - - let stream_id = stream_arg.to_u32(); - if stream_id == 0 { - return Err(global_object.throw(format_args!("Invalid stream id"))); - } - - let Some(stream) = this.streams.get().get(&stream_id).copied() else { - return Err(global_object.throw(format_args!("Invalid stream id"))); - }; + let stream = this.stream_from_js_arg::( + global_object, + args_list.ptr[0], + "Invalid stream id", + )?; // SAFETY: stream is a *mut Stream from self.streams (heap::alloc); valid while the map entry exists let stream = unsafe { &*stream }; @@ -5431,20 +5408,11 @@ impl H2FrameParser { if args_list.len < 1 { return Err(global_object.throw(format_args!("Expected stream argument"))); } - let stream_arg = args_list.ptr[0]; - - if !stream_arg.is_number() { - return Err(global_object.throw(format_args!("Invalid stream id"))); - } - - let stream_id = stream_arg.to_u32(); - if stream_id == 0 { - return Err(global_object.throw(format_args!("Invalid stream id"))); - } - - let Some(stream) = this.streams.get().get(&stream_id).copied() else { - return Err(global_object.throw(format_args!("Invalid stream id"))); - }; + let stream = this.stream_from_js_arg::( + global_object, + args_list.ptr[0], + "Invalid stream id", + )?; // SAFETY: stream is a *mut Stream from self.streams (heap::alloc); valid while the map entry exists let stream = unsafe { &mut *stream }; let state = JSValue::create_empty_object(global_object, 6); @@ -5494,21 +5462,13 @@ impl H2FrameParser { if args_list.len < 2 { return Err(global_object.throw(format_args!("Expected stream and options arguments"))); } - let stream_arg = args_list.ptr[0]; let options = args_list.ptr[1]; - if !stream_arg.is_number() { - return Err(global_object.throw(format_args!("Invalid stream id"))); - } - - let stream_id = stream_arg.to_u32(); - if stream_id == 0 { - return Err(global_object.throw(format_args!("Invalid stream id"))); - } - - let Some(stream_ptr) = this.streams.get().get(&stream_id).copied() else { - return Err(global_object.throw(format_args!("Invalid stream id"))); - }; + let stream_ptr = this.stream_from_js_arg::( + global_object, + args_list.ptr[0], + "Invalid stream id", + )?; // SAFETY: stream_ptr is a *mut Stream stored in self.streams (heap::alloc); valid for the lifetime of the entry, exclusive access reshaped for borrowck let stream = unsafe { &mut *stream_ptr }; @@ -5607,21 +5567,13 @@ impl H2FrameParser { if args_list.len < 2 { return Err(global_object.throw(format_args!("Expected stream and code arguments"))); } - let stream_arg = args_list.ptr[0]; let error_arg = args_list.ptr[1]; - if !stream_arg.is_number() { - return Err(global_object.throw(format_args!("Invalid stream id"))); - } - - let stream_id = stream_arg.to_u32(); - if stream_id == 0 || stream_id > MAX_STREAM_ID { - return Err(global_object.throw(format_args!("Invalid stream id"))); - } - - let Some(stream) = this.streams.get().get(&stream_id).copied() else { - return Err(global_object.throw(format_args!("Invalid stream id"))); - }; + let stream = this.stream_from_js_arg::( + global_object, + args_list.ptr[0], + "Invalid stream id", + )?; if !error_arg.is_number() { return Err(global_object.throw(format_args!("Invalid ErrorCode"))); } @@ -5842,20 +5794,11 @@ impl H2FrameParser { ))); } - let stream_arg = args_list.ptr[0]; - - if !stream_arg.is_number() { - return Err(global_object.throw(format_args!("Expected stream to be a number"))); - } - - let stream_id = stream_arg.to_u32(); - if stream_id == 0 || stream_id > MAX_STREAM_ID { - return Err(global_object.throw(format_args!("Invalid stream id"))); - } - - let Some(stream) = this.streams.get().get(&stream_id).copied() else { - return Err(global_object.throw(format_args!("Invalid stream id"))); - }; + let stream = this.stream_from_js_arg::( + global_object, + args_list.ptr[0], + "Expected stream to be a number", + )?; // SAFETY: stream is a *mut Stream from self.streams (heap::alloc); valid while the map entry exists let stream = unsafe { &mut *stream }; @@ -5889,23 +5832,7 @@ impl H2FrameParser { any = true; continue 'begin; } - b'a'..=b'z' - | b'0'..=b'9' - | b'!' - | b'#' - | b'$' - | b'%' - | b'&' - | b'\'' - | b'*' - | b'+' - | b'-' - | b'.' - | b'^' - | b'_' - | b'`' - | b'|' - | b'~' => {} + c if is_lower_tchar(c) => {} b':' => { // only allow pseudoheaders at the beginning if i != 0 || any { @@ -5939,22 +5866,14 @@ impl H2FrameParser { ))); } - let stream_arg = args_list.ptr[0]; let headers_arg = args_list.ptr[1]; let sensitive_arg = args_list.ptr[2]; - if !stream_arg.is_number() { - return Err(global_object.throw(format_args!("Expected stream to be a number"))); - } - - let stream_id = stream_arg.to_u32(); - if stream_id == 0 || stream_id > MAX_STREAM_ID { - return Err(global_object.throw(format_args!("Invalid stream id"))); - } - - let Some(stream_ptr) = this.streams.get().get(&stream_id).copied() else { - return Err(global_object.throw(format_args!("Invalid stream id"))); - }; + let stream_ptr = this.stream_from_js_arg::( + global_object, + args_list.ptr[0], + "Expected stream to be a number", + )?; // SAFETY: stream_ptr is a *mut Stream stored in self.streams (heap::alloc); valid for the lifetime of the entry, exclusive access reshaped for borrowck let stream = unsafe { &mut *stream_ptr }; @@ -6034,7 +5953,7 @@ impl H2FrameParser { value: &[u8], never_index: bool| -> JsResult> { - if !is_valid_header_value(value) { + if is_malformed_field_value(value) { let exception = global_object.to_type_error( bun_jsc::ErrorCode::HTTP2_INVALID_HEADER_VALUE, format_args!("Invalid value for header \"{}\"", BStr::new(validated_name)), @@ -6276,19 +6195,14 @@ impl H2FrameParser { let args = callframe.arguments_undef::<5>(); let [stream_arg, data_arg, encoding_arg, close_arg, callback_arg] = args.ptr; - if !stream_arg.is_number() { - return Err(global_object.throw(format_args!("Expected stream to be a number"))); - } - - let stream_id = stream_arg.to_u32(); - if stream_id == 0 || stream_id > MAX_STREAM_ID { - return Err(global_object.throw(format_args!("Invalid stream id"))); - } + let stream_ptr = this.stream_from_js_arg::( + global_object, + stream_arg, + "Expected stream to be a number", + )?; + // ToBoolean is side-effect free, so reading `close` after the stream + // lookup is observably identical to the previous ordering. let close = close_arg.to_boolean(); - - let Some(stream_ptr) = this.streams.get().get(&stream_id).copied() else { - return Err(global_object.throw(format_args!("Invalid stream id"))); - }; // SAFETY: stream_ptr is a *mut Stream stored in self.streams (heap::alloc); valid for the lifetime of the entry, exclusive access reshaped for borrowck let stream = unsafe { &mut *stream_ptr }; if !stream.can_send_data() { @@ -6425,14 +6339,11 @@ impl H2FrameParser { return Err(global_object.throw(format_args!("Expected stream_id argument"))); } - let stream_id_arg = args_list.ptr[0]; - if !stream_id_arg.is_number() { - return Err(global_object.throw(format_args!("Expected stream_id to be a number"))); - } - - let Some(stream) = this.streams.get().get(&stream_id_arg.to_u32()).copied() else { - return Err(global_object.throw(format_args!("Invalid stream id"))); - }; + let stream = this.stream_from_js_arg::( + global_object, + args_list.ptr[0], + "Expected stream_id to be a number", + )?; // SAFETY: stream is *mut Stream from self.streams; valid while the map entry exists Ok(unsafe { (*stream).js_context.get() }.unwrap_or(JSValue::UNDEFINED)) @@ -6451,13 +6362,11 @@ impl H2FrameParser { ); } - let stream_id_arg = args_list.ptr[0]; - if !stream_id_arg.is_number() { - return Err(global_object.throw(format_args!("Expected stream_id to be a number"))); - } - let Some(stream) = this.streams.get().get(&stream_id_arg.to_u32()).copied() else { - return Err(global_object.throw(format_args!("Invalid stream id"))); - }; + let stream = this.stream_from_js_arg::( + global_object, + args_list.ptr[0], + "Expected stream_id to be a number", + )?; let context_arg = args_list.ptr[1]; if !context_arg.is_object() { return Err(global_object.throw(format_args!("Expected context to be an object"))); @@ -6700,131 +6609,15 @@ impl H2FrameParser { return Err(global_object.throw_value(exception)); } - if js_value.js_type().is_array() { - bun_output::scoped_log!(H2FrameParser, "array header {}", BStr::new(name)); - let mut value_iter = js_value.array_iterator(global_object)?; - - if let Some(idx) = single_value_headers_index_of(validated_name) { - if value_iter.len > 1 || single_value_headers[idx] { - if !global_object.has_exception() { - let exception = global_object.to_type_error( - bun_jsc::ErrorCode::HTTP2_HEADER_SINGLE_VALUE, - format_args!( - "Header field \"{}\" must only have a single value", - BStr::new(validated_name) - ), - ); - return Err(global_object.throw_value(exception)); - } - return Ok(JSValue::ZERO); - } - single_value_headers[idx] = true; - } - - while let Some(item) = value_iter.next()? { - if item.is_empty_or_undefined_or_null() { - if !global_object.has_exception() { - return Err(global_object - .err( - JscErrorCode::HTTP2_INVALID_HEADER_VALUE, - format_args!( - "Invalid value for header \"{}\"", - BStr::new(validated_name) - ), - ) - .throw()); - } - return Ok(JSValue::ZERO); - } - - let value_str = match item.to_js_string(global_object) { - Ok(s) => s, - Err(_) => { - global_object.clear_exception(); - return Err(global_object - .err( - JscErrorCode::HTTP2_INVALID_HEADER_VALUE, - format_args!( - "Invalid value for header \"{}\"", - BStr::new(validated_name) - ), - ) - .throw()); - } - }; - - let never_index = - match sensitive_arg.get_truthy(global_object, validated_name)? { - Some(_) => true, - None => sensitive_arg.get_truthy(global_object, name)?.is_some(), - }; - - let value_slice = value_str.to_slice(global_object); - let value = value_slice.slice(); - if !is_valid_header_value(value) { - return Err(global_object - .err( - JscErrorCode::HTTP2_INVALID_HEADER_VALUE, - format_args!( - "Invalid value for header \"{}\"", - BStr::new(validated_name) - ), - ) - .throw()); - } - bun_output::scoped_log!( - H2FrameParser, - "encode header {} {}", - BStr::new(validated_name), - BStr::new(value) - ); - - if let Err(err) = this.encode_header_into_list( - &mut encoded_headers, - validated_name, - value, - never_index, - ) { - if err == bun_core::err!("OutOfMemory") { - return Err(global_object - .throw(format_args!("Failed to allocate header buffer"))); - } - let Some(stream) = this.handle_received_stream_id(stream_id) else { - return Ok(JSValue::js_number(-1.0)); - }; - // SAFETY: stream is a *mut Stream from self.streams (heap::alloc); valid while the map entry exists - let stream = unsafe { &mut *stream }; - if !stream_ctx_arg.is_empty_or_undefined_or_null() - && stream_ctx_arg.is_object() - { - stream.set_context(stream_ctx_arg, global_object); - } - stream.state = StreamState::CLOSED; - stream.rst_code = ErrorCode::COMPRESSION_ERROR.0; - this.dispatch_with_extra( - JSH2FrameParser::Gc::onStreamError, - stream.get_identifier(), - JSValue::js_number(stream.rst_code as f64), - ); - return Ok(JSValue::UNDEFINED); - } - } - } else if !js_value.is_empty_or_undefined_or_null() { - bun_output::scoped_log!(H2FrameParser, "single header {}", BStr::new(name)); - if let Some(idx) = single_value_headers_index_of(validated_name) { - if single_value_headers[idx] { - let exception = global_object.to_type_error( - bun_jsc::ErrorCode::HTTP2_HEADER_SINGLE_VALUE, - format_args!( - "Header field \"{}\" must only have a single value", - BStr::new(validated_name) - ), - ); - return Err(global_object.throw_value(exception)); - } - single_value_headers[idx] = true; - } - let value_str = match js_value.to_js_string(global_object) { + // closure shared by the array and single-value arms; `err_name` + // preserves each arm's user-visible message when string coercion + // fails, and `encode_err_return` its return value on a compression + // error (both match the reference implementation) + let mut encode_value = |item: JSValue, + err_name: &[u8], + encode_err_return: JSValue| + -> JsResult> { + let value_str = match item.to_js_string(global_object) { Ok(s) => s, Err(_) => { global_object.clear_exception(); @@ -6833,7 +6626,7 @@ impl H2FrameParser { JscErrorCode::HTTP2_INVALID_HEADER_VALUE, format_args!( "Invalid value for header \"{}\"", - BStr::new(name) + BStr::new(err_name) ), ) .throw()); @@ -6848,7 +6641,7 @@ impl H2FrameParser { let value_slice = value_str.to_slice(global_object); let value = value_slice.slice(); - if !is_valid_header_value(value) { + if is_malformed_field_value(value) { return Err(global_object .err( JscErrorCode::HTTP2_INVALID_HEADER_VALUE, @@ -6877,23 +6670,87 @@ impl H2FrameParser { .throw(format_args!("Failed to allocate header buffer"))); } let Some(stream) = this.handle_received_stream_id(stream_id) else { - return Ok(JSValue::js_number(-1.0)); + return Ok(Some(JSValue::js_number(-1.0))); }; // SAFETY: stream is a *mut Stream from self.streams (heap::alloc); valid while the map entry exists let stream = unsafe { &mut *stream }; - stream.state = StreamState::CLOSED; if !stream_ctx_arg.is_empty_or_undefined_or_null() && stream_ctx_arg.is_object() { stream.set_context(stream_ctx_arg, global_object); } + stream.state = StreamState::CLOSED; stream.rst_code = ErrorCode::COMPRESSION_ERROR.0; this.dispatch_with_extra( JSH2FrameParser::Gc::onStreamError, stream.get_identifier(), JSValue::js_number(stream.rst_code as f64), ); - return Ok(JSValue::js_number(stream_id as f64)); + return Ok(Some(encode_err_return)); + } + Ok(None) + }; + + if js_value.js_type().is_array() { + bun_output::scoped_log!(H2FrameParser, "array header {}", BStr::new(name)); + let mut value_iter = js_value.array_iterator(global_object)?; + + if let Some(idx) = single_value_headers_index_of(validated_name) { + if value_iter.len > 1 || single_value_headers[idx] { + if !global_object.has_exception() { + let exception = global_object.to_type_error( + bun_jsc::ErrorCode::HTTP2_HEADER_SINGLE_VALUE, + format_args!( + "Header field \"{}\" must only have a single value", + BStr::new(validated_name) + ), + ); + return Err(global_object.throw_value(exception)); + } + return Ok(JSValue::ZERO); + } + single_value_headers[idx] = true; + } + + while let Some(item) = value_iter.next()? { + if item.is_empty_or_undefined_or_null() { + if !global_object.has_exception() { + return Err(global_object + .err( + JscErrorCode::HTTP2_INVALID_HEADER_VALUE, + format_args!( + "Invalid value for header \"{}\"", + BStr::new(validated_name) + ), + ) + .throw()); + } + return Ok(JSValue::ZERO); + } + + if let Some(ret) = encode_value(item, validated_name, JSValue::UNDEFINED)? { + return Ok(ret); + } + } + } else if !js_value.is_empty_or_undefined_or_null() { + bun_output::scoped_log!(H2FrameParser, "single header {}", BStr::new(name)); + if let Some(idx) = single_value_headers_index_of(validated_name) { + if single_value_headers[idx] { + let exception = global_object.to_type_error( + bun_jsc::ErrorCode::HTTP2_HEADER_SINGLE_VALUE, + format_args!( + "Header field \"{}\" must only have a single value", + BStr::new(validated_name) + ), + ); + return Err(global_object.throw_value(exception)); + } + single_value_headers[idx] = true; + } + if let Some(ret) = + encode_value(js_value, name, JSValue::js_number(stream_id as f64))? + { + return Ok(ret); } } } diff --git a/src/runtime/api/bun/subprocess/Writable.rs b/src/runtime/api/bun/subprocess/Writable.rs index 0ec7ac6e134b..4fd9297bb57b 100644 --- a/src/runtime/api/bun/subprocess/Writable.rs +++ b/src/runtime/api/bun/subprocess/Writable.rs @@ -17,6 +17,37 @@ use bun_io::pipe_writer::BaseWindowsPipeWriter as _; use super::{Flags, StaticPipeWriter, StdioResult, Subprocess, js}; +/// Build the `Writable::Buffer` writer for a `Stdio::Blob` / +/// `Stdio::ArrayBuffer` stdin, leaving `Stdio::Ignore` behind. Shared by the +/// `Bun.spawn` and shell `Writable::init` (both platform arms of each). +pub(crate) fn buffered_stdin_writer( + stdio: &mut Stdio, + event_loop: bun_event_loop::EventLoopHandle, + process: *mut P, + result: StdioResult, +) -> RefPtr> { + let source = match stdio { + Stdio::Blob(_) => { + // `Stdio` has a Drop impl (it would `blob.detach()`), so the + // payload cannot be destructure-moved out (E0509); take ownership + // via ManuallyDrop + ptr::read so the blob is moved exactly once. + let owned = core::mem::ManuallyDrop::new(core::mem::replace(stdio, Stdio::Ignore)); + let blob = match &*owned { + // SAFETY: `owned` is ManuallyDrop and discarded after this + // read; the Blob payload is moved out exactly once. + Stdio::Blob(b) => unsafe { core::ptr::read(b) }, + _ => unreachable!(), + }; + super::source_from_blob(blob) + } + Stdio::ArrayBuffer(array_buffer) => { + super::source_from_array_buffer(core::mem::take(array_buffer)) + } + _ => unreachable!("caller matched Blob/ArrayBuffer"), + }; + super::NewStaticPipeWriter::create(event_loop, process, result, source) +} + pub enum Writable<'a> { // `FileSink` is intrusive-refcounted (manual ref/deref): keep a raw // NonNull and call `FileSink::deref` explicitly. @@ -247,29 +278,12 @@ impl<'a> Writable<'a> { return Ok(Writable::Inherit); } - Stdio::Blob(_) => { - // See the unix arm below: Stdio has Drop, so move the - // payload out via ManuallyDrop + ptr::read. - let owned = - core::mem::ManuallyDrop::new(core::mem::replace(stdio, Stdio::Ignore)); - let blob = match &*owned { - // SAFETY: owned is ManuallyDrop; payload moved exactly once. - Stdio::Blob(b) => unsafe { core::ptr::read(b) }, - _ => unreachable!(), - }; - return Ok(Writable::Buffer(StaticPipeWriter::create( + Stdio::Blob(_) | Stdio::ArrayBuffer(_) => { + return Ok(Writable::Buffer(buffered_stdin_writer( + stdio, evtloop, subprocess as *mut Subprocess<'a>, result, - super::source_from_blob(blob), - ))); - } - Stdio::ArrayBuffer(array_buffer) => { - return Ok(Writable::Buffer(StaticPipeWriter::create( - evtloop, - subprocess as *mut Subprocess<'a>, - result, - super::source_from_array_buffer(core::mem::take(array_buffer)), ))); } Stdio::Fd(fd) => { @@ -349,29 +363,11 @@ impl<'a> Writable<'a> { Ok(Writable::Pipe(pipe_nn)) } - Stdio::Blob(_) => { - // `Stdio` has a Drop impl (would `blob.detach()`), so we can't - // move the payload out by match — take ownership via - // ManuallyDrop + ptr::read to transfer without detaching. - let owned = core::mem::ManuallyDrop::new(core::mem::replace(stdio, Stdio::Ignore)); - let blob = match &*owned { - // SAFETY: `owned` is ManuallyDrop and discarded after this - // read; the Blob payload is moved out exactly once. - Stdio::Blob(b) => unsafe { core::ptr::read(b) }, - _ => unreachable!(), - }; - Ok(Writable::Buffer(StaticPipeWriter::create( - evtloop, - std::ptr::from_mut::>(subprocess), - result, - super::source_from_blob(blob), - ))) - } - Stdio::ArrayBuffer(array_buffer) => Ok(Writable::Buffer(StaticPipeWriter::create( + Stdio::Blob(_) | Stdio::ArrayBuffer(_) => Ok(Writable::Buffer(buffered_stdin_writer( + stdio, evtloop, std::ptr::from_mut::>(subprocess), result, - super::source_from_array_buffer(core::mem::take(array_buffer)), ))), Stdio::Memfd(_) => { // Transfer ownership: `Stdio`'s Drop would close the memfd, so diff --git a/src/runtime/api/cron.rs b/src/runtime/api/cron.rs index b2f47266199e..c9dc485ff9a7 100644 --- a/src/runtime/api/cron.rs +++ b/src/runtime/api/cron.rs @@ -64,87 +64,29 @@ use crate::jsc_hooks::timer_all_mut as timer_all; // CronJobBase — shared base for CronRegisterJob and CronRemoveJob // ============================================================================ -/// Shared base for [`CronRegisterJob`] and [`CronRemoveJob`]. -// Note: every method on the path to `finish()` (which `heap::take`- -// drops `this`) takes a raw `*mut Self` receiver. -// A `&mut self` *parameter* would carry a Stacked Borrows FnEntry protector, -// making the in-flight dealloc UB; a *local* `let s = &mut *this` reborrow -// has no protector and ends at last use under NLL, so field access via `s` -// followed by `Self::finish(this)` is sound. -trait CronJobBase: Sized { - fn remaining_fds_mut(&mut self) -> &mut i8; - fn err_msg_mut(&mut self) -> &mut Option>; - fn has_called_process_exit_mut(&mut self) -> &mut bool; - fn exit_status_mut(&mut self) -> &mut Option; - /// May free `this`. Caller must not touch `this` afterward. - unsafe fn maybe_finished(this: *mut Self); - - fn loop_(&self) -> *mut AsyncLoop { - // `VirtualMachine::uv_loop` already returns the native loop on both - // targets (jsc/VirtualMachine.rs:2975); the prior POSIX arm's - // `bun_uws::Loop::get()` named the same per-thread singleton. - vm_mut().uv_loop() - } - - /// May free `this` via `maybe_finished`. - unsafe fn on_reader_done(this: *mut Self) { - // SAFETY: local reborrow, no protector; ends before `maybe_finished`. - let s = unsafe { &mut *this }; - debug_assert!(*s.remaining_fds_mut() > 0); - *s.remaining_fds_mut() -= 1; - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::maybe_finished(this) }; - } - - /// May free `this` via `maybe_finished`. - unsafe fn on_reader_error(this: *mut Self, err: sys::Error) { - // SAFETY: local reborrow, no protector; ends before `maybe_finished`. - let s = unsafe { &mut *this }; - debug_assert!(*s.remaining_fds_mut() > 0); - *s.remaining_fds_mut() -= 1; - if s.err_msg_mut().is_none() { - let mut msg = Vec::new(); - let _ = write!( - &mut msg, - "Failed to read process output: {}", - <&'static str>::from(err.get_errno()) - ); - *s.err_msg_mut() = Some(msg); - } - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::maybe_finished(this) }; - } - - /// May free `this` via `maybe_finished`. - unsafe fn on_process_exit(this: *mut Self, _proc: &Process, status: Status, _rusage: &Rusage) { - // SAFETY: local reborrow, no protector; ends before `maybe_finished`. - let s = unsafe { &mut *this }; - *s.has_called_process_exit_mut() = true; - *s.exit_status_mut() = Some(status); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::maybe_finished(this) }; - } +#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq)] +enum CronJobState { + ReadingCrontab, + InstallingCrontab, + #[cfg(target_os = "macos")] + WritingPlist, + BootingOut, + #[cfg(target_os = "macos")] + Bootstrapping, + Done, + Failed, } -// ============================================================================ -// CronRegisterJob -// ============================================================================ - -pub struct CronRegisterJob { +/// Fields shared by [`CronRegisterJob`] and [`CronRemoveJob`]. +struct CronJobCommon { promise: jsc::JSPromiseStrong, // LIFETIMES.tsv: JSC_BORROW → GlobalRef global: GlobalRef, poll: KeepAlive, - - bun_exe: &'static ZStr, - abs_path: ZString, - /// normalized numeric form for crontab/launchd - schedule: ZString, title: ZString, - #[cfg(windows)] - parsed_cron: CronExpression, - state: RegisterState, + state: CronJobState, // LIFETIMES.tsv: SHARED — `Process` is intrusively refcounted (`*mut`). process: Option<*mut Process>, stdout_reader: OutputReader, @@ -160,95 +102,158 @@ pub struct CronRegisterJob { event_loop_handle: EventLoopHandle, } -#[repr(u8)] -#[derive(Clone, Copy, PartialEq, Eq)] -enum RegisterState { - ReadingCrontab, - #[cfg(not(target_os = "macos"))] - InstallingCrontab, - #[cfg(target_os = "macos")] - WritingPlist, - BootingOut, - #[cfg(target_os = "macos")] - Bootstrapping, - Done, - Failed, +impl CronJobCommon { + /// `T` is the concrete job type owning this base (reader-parent vtable). + fn init(global: &JSGlobalObject, title: &[u8]) -> Self { + Self { + promise: jsc::JSPromiseStrong::init(global), + global: GlobalRef::from(global), + poll: KeepAlive::default(), + title: ZString::from_bytes(title), + state: CronJobState::ReadingCrontab, + process: None, + stdout_reader: OutputReader::init::(), + #[cfg(windows)] + stderr_reader: OutputReader::init::(), + remaining_fds: 0, + has_called_process_exit: false, + exit_status: None, + err_msg: None, + tmp_path: None, + // SAFETY: `vm_mut().event_loop()` returns the live per-thread `jsc::EventLoop`. + event_loop_handle: EventLoopHandle::init(vm_mut().event_loop().cast::<()>()), + } + } + + fn set_err(&mut self, args: core::fmt::Arguments<'_>) { + if self.err_msg.is_none() { + let mut msg = Vec::new(); + let _ = msg.write_fmt(args); + self.err_msg = Some(msg); + } + } + + fn detach_process(&mut self) { + if let Some(proc) = self.process.take() { + // SAFETY: `proc` is the intrusive-RC pointer returned by `to_process`. + unsafe { + (*proc).detach(); + Process::deref(proc); + } + } + } } -// Forward as raw ptr — `maybe_finished` (via `CronJobBase`) may free `this`. -bun_io::impl_buffered_reader_parent! { - CronRegister for CronRegisterJob; - has_on_read_chunk = false; - on_reader_done = |this| ::on_reader_done(this); - on_reader_error = |this, err| ::on_reader_error(this, err); - loop_ = |this| ::loop_(&*this).cast(); - event_loop = |this| (*this).event_loop_handle.as_event_loop_ctx(); +impl Drop for CronJobCommon { + fn drop(&mut self) { + // stdout_reader / stderr_reader drop via their own Drop. + self.detach_process(); + if let Some(p) = self.tmp_path.take() { + let _ = sys::unlink(&p); + } + // err_msg, title freed via field Drop. + } } -impl CronJobBase for CronRegisterJob { - fn remaining_fds_mut(&mut self) -> &mut i8 { - &mut self.remaining_fds +/// Shared base for [`CronRegisterJob`] and [`CronRemoveJob`]. +// Note: every method on the path to `finish()` (which `heap::take`- +// drops `this`) takes a raw `*mut Self` receiver. +// A `&mut self` *parameter* would carry a Stacked Borrows FnEntry protector, +// making the in-flight dealloc UB; a *local* `let s = &mut *this` reborrow +// has no protector and ends at last use under NLL, so field access via `s` +// followed by `Self::finish(this)` is sound. +trait CronJobBase: Sized + BufferedReaderParent { + const EXIT_KIND: bun_spawn::ProcessExitKind; + fn base(&self) -> &CronJobCommon; + fn base_mut(&mut self) -> &mut CronJobCommon; + /// Dispatch on the state machine after a clean process exit. + /// May free `this`. Caller must not touch `this` afterward. + unsafe fn advance_state(this: *mut Self); + + /// Whether a nonzero exit code is benign in the current state: an empty + /// crontab makes `crontab -l` exit 1, and `launchctl bootout` fails when + /// the job was not loaded. + fn accepts_nonzero_exit(&self, code: u8) -> bool { + let state = self.base().state; + (state == CronJobState::ReadingCrontab && code == 1) || state == CronJobState::BootingOut } - fn err_msg_mut(&mut self) -> &mut Option> { - &mut self.err_msg + + /// Hook for a job-specific error message derived from stderr; returns + /// true if it consumed the failure (an error was set). + #[cfg(windows)] + fn exit_err_override(&mut self, stderr: &[u8]) -> bool { + let _ = stderr; + false } - fn has_called_process_exit_mut(&mut self) -> &mut bool { - &mut self.has_called_process_exit + + fn loop_(&self) -> *mut AsyncLoop { + // `VirtualMachine::uv_loop` already returns the native loop on both + // targets (jsc/VirtualMachine.rs:2975); the prior POSIX arm's + // `bun_uws::Loop::get()` named the same per-thread singleton. + vm_mut().uv_loop() } - fn exit_status_mut(&mut self) -> &mut Option { - &mut self.exit_status + + /// May free `this` via `maybe_finished`. + unsafe fn on_reader_done(this: *mut Self) { + // SAFETY: local reborrow, no protector; ends before `maybe_finished`. + let b = unsafe { &mut *this }.base_mut(); + debug_assert!(b.remaining_fds > 0); + b.remaining_fds -= 1; + // SAFETY: local reborrow has ended; `this` is the live heap job. + unsafe { Self::maybe_finished(this) }; } - unsafe fn maybe_finished(this: *mut Self) { - // SAFETY: caller guarantees `this` is the live heap job with no active borrows. - unsafe { CronRegisterJob::maybe_finished(this) } + + /// May free `this` via `maybe_finished`. + unsafe fn on_reader_error(this: *mut Self, err: sys::Error) { + // SAFETY: local reborrow, no protector; ends before `maybe_finished`. + let b = unsafe { &mut *this }.base_mut(); + debug_assert!(b.remaining_fds > 0); + b.remaining_fds -= 1; + b.set_err(format_args!( + "Failed to read process output: {}", + <&'static str>::from(err.get_errno()) + )); + // SAFETY: local reborrow has ended; `this` is the live heap job. + unsafe { Self::maybe_finished(this) }; } -} -impl CronRegisterJob { - fn set_err(&mut self, args: core::fmt::Arguments<'_>) { - if self.err_msg.is_none() { - let mut msg = Vec::new(); - let _ = msg.write_fmt(args); - self.err_msg = Some(msg); - } + /// May free `this` via `maybe_finished`. + unsafe fn on_process_exit(this: *mut Self, _proc: &Process, status: Status, _rusage: &Rusage) { + // SAFETY: local reborrow, no protector; ends before `maybe_finished`. + let b = unsafe { &mut *this }.base_mut(); + b.has_called_process_exit = true; + b.exit_status = Some(status); + // SAFETY: local reborrow has ended; `this` is the live heap job. + unsafe { Self::maybe_finished(this) }; } - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. + /// May free `this`. Raw-ptr receiver: see trait-level note. unsafe fn maybe_finished(this: *mut Self) { // SAFETY: local reborrow (no FnEntry protector); not used after any // call below that may free `this`. let s = unsafe { &mut *this }; - if !s.has_called_process_exit || s.remaining_fds != 0 { + if !s.base().has_called_process_exit || s.base().remaining_fds != 0 { return; } - if let Some(proc) = s.process.take() { - // SAFETY: `proc` is the intrusive-RC pointer returned by `to_process`. - unsafe { - (*proc).detach(); - Process::deref(proc); - } - } - if s.err_msg.is_some() { + s.base_mut().detach_process(); + if s.base().err_msg.is_some() { // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } - let Some(status) = s.exit_status.take() else { + let Some(status) = s.base_mut().exit_status.take() else { return; }; match status { Status::Exited(exited) => { - if exited.code != 0 - && !(s.state == RegisterState::ReadingCrontab && exited.code == 1) - && s.state != RegisterState::BootingOut - { + if exited.code != 0 && !s.accepts_nonzero_exit(exited.code) { // Materialize the trimmed stderr into an owned buffer: - // `final_buffer()` borrows `s` mutably, and `set_err` - // below needs another `&mut s` — copy out so the two + // `final_buffer()` borrows the base mutably, and `set_err` + // below needs another `&mut` — copy out so the two // borrows do not overlap (Windows only; POSIX ignores // stderr here). #[cfg(windows)] let stderr_owned: Vec = bun_core::immutable::trim( - s.stderr_reader.final_buffer().as_slice(), + s.base_mut().stderr_reader.final_buffer().as_slice(), &ASCII_WHITESPACE, ) .to_vec(); @@ -256,44 +261,32 @@ impl CronRegisterJob { let stderr_output: &[u8] = stderr_owned.as_slice(); #[cfg(not(windows))] let stderr_output: &[u8] = b""; - // On Windows, detect the SID resolution error and provide - // a clear message instead of the raw schtasks output. #[cfg(windows)] - { - if s.state == RegisterState::InstallingCrontab - && bun_core::index_of( - stderr_output, - b"No mapping between account names", - ) - .is_some() - { - s.set_err(format_args!( - "Failed to register cron job: your Windows account's Security Identifier (SID) could not be resolved. \ - This typically happens on headless servers or CI where the process runs under a service account. \ - To fix this, either run Bun as a regular user account, or create the scheduled task manually with: \ - schtasks /create /xml /tn /ru SYSTEM /f" - )); - return unsafe { Self::finish(this) }; - } + if s.exit_err_override(stderr_output) { + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + return unsafe { Self::finish(this) }; } if !stderr_output.is_empty() { - s.set_err(format_args!("{}", bstr::BStr::new(stderr_output))); + s.base_mut() + .set_err(format_args!("{}", bstr::BStr::new(stderr_output))); } else { - s.set_err(format_args!("Process exited with code {}", exited.code)); + s.base_mut() + .set_err(format_args!("Process exited with code {}", exited.code)); } // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } } Status::Signaled(sig) => { - if s.state != RegisterState::BootingOut { - s.set_err(format_args!("Process killed by signal {}", sig as i32)); + if s.base().state != CronJobState::BootingOut { + s.base_mut() + .set_err(format_args!("Process killed by signal {}", sig as i32)); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } } Status::Err(err) => { - s.set_err(format_args!( + s.base_mut().set_err(format_args!( "Process error: {}", <&'static str>::from(err.get_errno()) )); @@ -306,70 +299,31 @@ impl CronRegisterJob { unsafe { Self::advance_state(this) }; } - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. - unsafe fn advance_state(this: *mut Self) { - // SAFETY: local reborrow; last use precedes any self-freeing call. - let s = unsafe { &mut *this }; - #[cfg(target_os = "macos")] - { - match s.state { - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - RegisterState::WritingPlist => unsafe { Self::spawn_bootout(this) }, - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - RegisterState::BootingOut => unsafe { Self::spawn_bootstrap(this) }, - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - RegisterState::Bootstrapping => unsafe { Self::finish(this) }, - _ => { - s.set_err(format_args!("Unexpected state")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::finish(this) }; - } - } - } - #[cfg(not(target_os = "macos"))] - { - match s.state { - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - RegisterState::ReadingCrontab => unsafe { Self::process_crontab_and_install(this) }, - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - RegisterState::InstallingCrontab => unsafe { Self::finish(this) }, - _ => { - s.set_err(format_args!("Unexpected state")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::finish(this) }; - } - } - } - } - /// Consumes and frees `this` (`heap::take`). unsafe fn finish(this: *mut Self) { // SAFETY: caller holds the unique Box; consumed below. Local // reborrow has no FnEntry protector and is not used after the drop. - let this_ref = unsafe { &mut *this }; - this_ref.state = if this_ref.err_msg.is_some() { - RegisterState::Failed + let b = unsafe { &mut *this }.base_mut(); + b.state = if b.err_msg.is_some() { + CronJobState::Failed } else { - RegisterState::Done + CronJobState::Done }; - this_ref.poll.unref(bun_io::js_vm_ctx()); + b.poll.unref(bun_io::js_vm_ctx()); let ev = VirtualMachine::get().event_loop_mut(); ev.enter(); - if let Some(msg) = &this_ref.err_msg { - let _ = this_ref.promise.reject_with_async_stack( - &this_ref.global, - Ok(this_ref - .global + if let Some(msg) = &b.err_msg { + let _ = b.promise.reject_with_async_stack( + &b.global, + Ok(b.global .create_error_instance(format_args!("{}", bstr::BStr::new(msg)))), ); } else { - let _ = this_ref - .promise - .resolve(&this_ref.global, JSValue::UNDEFINED); + let _ = b.promise.resolve(&b.global, JSValue::UNDEFINED); } // Drop runs INSIDE the enter/exit scope so Process detach/deref and // reader teardown observe the entered event-loop state. - // SAFETY: `this` was created via heap::alloc in cron_register. + // SAFETY: `this` was created via heap::alloc in cron_register/cron_remove. unsafe { drop(bun_core::heap::take(this)) }; ev.exit(); } @@ -385,101 +339,252 @@ impl CronRegisterJob { unsafe { spawn_cmd_generic(this, argv, stdin_opt, stdout_opt) }; } - // -- Linux -- - - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. + /// Spawn `crontab -l` and buffer its output. May free `this`. #[cfg(all(not(target_os = "macos"), not(windows)))] unsafe fn start_linux(this: *mut Self) { // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - s.state = RegisterState::ReadingCrontab; - s.stdout_reader = OutputReader::init::(); - s.stdout_reader.set_parent(this.cast()); + let b = unsafe { &mut *this }.base_mut(); + b.state = CronJobState::ReadingCrontab; + b.stdout_reader = OutputReader::init::(); + b.stdout_reader.set_parent(this.cast()); let Some(crontab_path) = find_crontab() else { - s.set_err(format_args!("crontab not found in PATH")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + b.set_err(format_args!("crontab not found in PATH")); + // SAFETY: local reborrow has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; }; let mut argv: [*const c_char; 3] = [crontab_path, c"-l".as_ptr(), core::ptr::null()]; - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + // SAFETY: local reborrow has ended; `this` is the live heap job. unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Buffer) }; } - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. + /// Read the captured `crontab -l` output and drop any existing entry for + /// `title`. On allocation failure, fails the job (freeing `this`) and + /// returns `None`. #[cfg(not(target_os = "macos"))] - unsafe fn process_crontab_and_install(this: *mut Self) { - // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - let existing_content = s.stdout_reader.final_buffer().as_slice(); + unsafe fn take_filtered_crontab(this: *mut Self) -> Option> { + // SAFETY: local reborrow; not used after `finish`. + let b = unsafe { &mut *this }.base_mut(); + let existing_content = b.stdout_reader.final_buffer().as_slice(); let mut result: Vec = Vec::new(); - - if filter_crontab(existing_content, s.title.as_bytes(), &mut result).is_err() { - s.set_err(format_args!("Out of memory building crontab")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - - // Build new entry with single-quoted paths to prevent shell injection - let mut new_entry = Vec::new(); - if write!( - &mut new_entry, - "# bun-cron: {title}\n{sched} '{exe}' run --cron-title={title} --cron-period='{sched}' '{path}'\n", - title = bstr::BStr::new(s.title.as_bytes()), - sched = bstr::BStr::new(s.schedule.as_bytes()), - exe = bstr::BStr::new(s.bun_exe.as_bytes()), - path = bstr::BStr::new(s.abs_path.as_bytes()), - ) - .is_err() - { - s.set_err(format_args!("Out of memory")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + if filter_crontab(existing_content, b.title.as_bytes(), &mut result).is_err() { + b.set_err(format_args!("Out of memory building crontab")); + // SAFETY: local reborrow has ended; `this` is the live heap job. + unsafe { Self::finish(this) }; + return None; } - result.extend_from_slice(&new_entry); + Some(result) + } - let tmp_path = match make_temp_path("bun-cron-") { + /// Write `content` to a fresh temp file and spawn `crontab ` to + /// install it. May free `this`. + #[cfg(not(target_os = "macos"))] + unsafe fn install_crontab(this: *mut Self, content: &[u8], tmp_prefix: &'static str) { + // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. + let b = unsafe { &mut *this }.base_mut(); + let tmp_path = match make_temp_path(tmp_prefix) { Ok(p) => p, Err(_) => { - s.set_err(format_args!("Out of memory")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + b.set_err(format_args!("Out of memory")); + // SAFETY: local reborrow has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } }; let tmp_path_ptr = tmp_path.as_ptr(); - s.tmp_path = Some(tmp_path); + b.tmp_path = Some(tmp_path); let file = match File::openat( Fd::cwd(), - s.tmp_path.as_ref().unwrap(), + b.tmp_path.as_ref().unwrap(), sys::O::WRONLY | sys::O::CREAT | sys::O::EXCL, 0o600, ) { Ok(f) => f, Err(_) => { - s.set_err(format_args!("Failed to create temp file")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + b.set_err(format_args!("Failed to create temp file")); + // SAFETY: local reborrow has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } }; - if file.write_all(&result).is_err() { + if file.write_all(content).is_err() { let _ = file.close(); // close error is non-actionable - s.set_err(format_args!("Failed to write temp file")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + b.set_err(format_args!("Failed to write temp file")); + // SAFETY: local reborrow has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } let _ = file.close(); // close error is non-actionable - s.state = RegisterState::InstallingCrontab; + b.state = CronJobState::InstallingCrontab; // Note: explicit deinit of old reader before reassign — Drop handles it. - s.stdout_reader = OutputReader::init::(); + b.stdout_reader = OutputReader::init::(); let Some(crontab_path) = find_crontab() else { - s.set_err(format_args!("crontab not found in PATH")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + b.set_err(format_args!("crontab not found in PATH")); + // SAFETY: local reborrow has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; }; let mut argv: [*const c_char; 3] = [crontab_path, tmp_path_ptr.cast(), core::ptr::null()]; - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + // SAFETY: local reborrow has ended; `this` is the live heap job. + unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Ignore) }; + } + + /// Spawn `launchctl bootout` for this job's launchd label. May free `this`. + #[cfg(target_os = "macos")] + unsafe fn spawn_bootout(this: *mut Self) { + // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. + let b = unsafe { &mut *this }.base_mut(); + b.state = CronJobState::BootingOut; + let uid_str = match alloc_print_z(format_args!( + "gui/{}/bun.cron.{}", + get_uid(), + bstr::BStr::new(b.title.as_bytes()) + )) { + Ok(v) => v, + Err(_) => { + b.set_err(format_args!("Out of memory")); + // SAFETY: local reborrow has ended; `this` is the live heap job. + return unsafe { Self::finish(this) }; + } + }; + let mut argv: [*const c_char; 4] = [ + c"/bin/launchctl".as_ptr().cast(), + c"bootout".as_ptr().cast(), + uid_str.as_ptr().cast(), + core::ptr::null(), + ]; + // SAFETY: local reborrow has ended; `this` is the live heap job. unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Ignore) }; + drop(uid_str); + } +} + +/// Ref the keep-alive poll and grab the promise value before `start_*` runs +/// (which may synchronously free `job`). +/// +/// SAFETY: `job` must be the live, uniquely-owned heap job (freshly leaked Box). +unsafe fn arm_job(job: *mut T) -> JSValue { + // SAFETY: caller contract; short-lived borrow ends on return. + let b = unsafe { &mut *job }.base_mut(); + b.poll.ref_(bun_io::js_vm_ctx()); + b.promise.value() +} + +// ============================================================================ +// CronRegisterJob +// ============================================================================ + +pub struct CronRegisterJob { + base: CronJobCommon, + + bun_exe: &'static ZStr, + abs_path: ZString, + /// normalized numeric form for crontab/launchd + schedule: ZString, + #[cfg(windows)] + parsed_cron: CronExpression, +} + +// Forward as raw ptr — `maybe_finished` (via `CronJobBase`) may free `this`. +bun_io::impl_buffered_reader_parent! { + CronRegister for CronRegisterJob; + has_on_read_chunk = false; + on_reader_done = |this| ::on_reader_done(this); + on_reader_error = |this, err| ::on_reader_error(this, err); + loop_ = |this| ::loop_(&*this).cast(); + event_loop = |this| (*this).base.event_loop_handle.as_event_loop_ctx(); +} + +impl CronJobBase for CronRegisterJob { + const EXIT_KIND: bun_spawn::ProcessExitKind = bun_spawn::ProcessExitKind::CronRegister; + fn base(&self) -> &CronJobCommon { + &self.base + } + fn base_mut(&mut self) -> &mut CronJobCommon { + &mut self.base + } + + /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. + unsafe fn advance_state(this: *mut Self) { + // SAFETY: local reborrow; last use precedes any self-freeing call. + let s = unsafe { &mut *this }; + #[cfg(target_os = "macos")] + { + match s.base.state { + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + CronJobState::WritingPlist => unsafe { Self::spawn_bootout(this) }, + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + CronJobState::BootingOut => unsafe { Self::spawn_bootstrap(this) }, + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + CronJobState::Bootstrapping => unsafe { Self::finish(this) }, + _ => { + s.base.set_err(format_args!("Unexpected state")); + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + unsafe { Self::finish(this) }; + } + } + } + #[cfg(not(target_os = "macos"))] + { + match s.base.state { + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + CronJobState::ReadingCrontab => unsafe { Self::process_crontab_and_install(this) }, + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + CronJobState::InstallingCrontab => unsafe { Self::finish(this) }, + _ => { + s.base.set_err(format_args!("Unexpected state")); + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + unsafe { Self::finish(this) }; + } + } + } + } + + /// On Windows, detect the SID resolution error and provide a clear + /// message instead of the raw schtasks output. + #[cfg(windows)] + fn exit_err_override(&mut self, stderr: &[u8]) -> bool { + if self.base.state == CronJobState::InstallingCrontab + && bun_core::index_of(stderr, b"No mapping between account names").is_some() + { + self.base.set_err(format_args!( + "Failed to register cron job: your Windows account's Security Identifier (SID) could not be resolved. \ + This typically happens on headless servers or CI where the process runs under a service account. \ + To fix this, either run Bun as a regular user account, or create the scheduled task manually with: \ + schtasks /create /xml /tn /ru SYSTEM /f" + )); + return true; + } + false + } +} + +impl CronRegisterJob { + /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. + #[cfg(not(target_os = "macos"))] + unsafe fn process_crontab_and_install(this: *mut Self) { + // SAFETY: `this` is the live heap job; freed inside on failure. + let Some(mut result) = (unsafe { Self::take_filtered_crontab(this) }) else { + return; + }; + // SAFETY: local reborrow; not used after `install_crontab`/`finish`. + let s = unsafe { &mut *this }; + + // Build new entry with single-quoted paths to prevent shell injection + if write!( + &mut result, + "# bun-cron: {title}\n{sched} '{exe}' run --cron-title={title} --cron-period='{sched}' '{path}'\n", + title = bstr::BStr::new(s.base.title.as_bytes()), + sched = bstr::BStr::new(s.schedule.as_bytes()), + exe = bstr::BStr::new(s.bun_exe.as_bytes()), + path = bstr::BStr::new(s.abs_path.as_bytes()), + ) + .is_err() + { + s.base.set_err(format_args!("Out of memory")); + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + return unsafe { Self::finish(this) }; + } + + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + unsafe { Self::install_crontab(this, &result, "bun-cron-") }; } // -- macOS -- @@ -489,19 +594,20 @@ impl CronRegisterJob { unsafe fn start_mac(this: *mut Self) { // SAFETY: local reborrow; not used after `spawn_bootout`/`finish`. let s = unsafe { &mut *this }; - s.state = RegisterState::WritingPlist; + s.base.state = CronJobState::WritingPlist; let calendar_xml = match cron_to_calendar_interval(s.schedule.as_bytes()) { Ok(x) => x, Err(_) => { - s.set_err(format_args!("Invalid cron expression")); + s.base.set_err(format_args!("Invalid cron expression")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } }; let Some(home) = env_var::HOME.get() else { - s.set_err(format_args!("HOME environment variable not set")); + s.base + .set_err(format_args!("HOME environment variable not set")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; }; @@ -513,7 +619,7 @@ impl CronRegisterJob { bstr::BStr::new(home) ); if Fd::cwd().make_path(&launch_agents_dir).is_err() { - s.set_err(format_args!( + s.base.set_err(format_args!( "Failed to create ~/Library/LaunchAgents directory" )); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. @@ -523,16 +629,16 @@ impl CronRegisterJob { let plist_path = match alloc_print_z(format_args!( "{}/Library/LaunchAgents/bun.cron.{}.plist", bstr::BStr::new(home), - bstr::BStr::new(s.title.as_bytes()) + bstr::BStr::new(s.base.title.as_bytes()) )) { Ok(p) => p, Err(_) => { - s.set_err(format_args!("Out of memory")); + s.base.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } }; - s.tmp_path = Some(plist_path); + s.base.tmp_path = Some(plist_path); // XML-escape all dynamic values macro_rules! try_escape { @@ -540,14 +646,14 @@ impl CronRegisterJob { match xml_escape($e) { Ok(v) => v, Err(_) => { - s.set_err(format_args!("Out of memory")); + s.base.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } } }; } - let xml_title = try_escape!(s.title.as_bytes()); + let xml_title = try_escape!(s.base.title.as_bytes()); let xml_bun = try_escape!(s.bun_exe.as_bytes()); let xml_path = try_escape!(s.abs_path.as_bytes()); let xml_sched = try_escape!(s.schedule.as_bytes()); @@ -585,27 +691,27 @@ impl CronRegisterJob { ) .is_err() { - s.set_err(format_args!("Out of memory")); + s.base.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } let file = match File::openat( Fd::cwd(), - s.tmp_path.as_ref().unwrap(), + s.base.tmp_path.as_ref().unwrap(), sys::O::WRONLY | sys::O::CREAT | sys::O::TRUNC, 0o644, ) { Ok(f) => f, Err(_) => { - s.set_err(format_args!("Failed to create plist file")); + s.base.set_err(format_args!("Failed to create plist file")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } }; if file.write_all(&plist).is_err() { let _ = file.close(); // close error is non-actionable - s.set_err(format_args!("Failed to write plist")); + s.base.set_err(format_args!("Failed to write plist")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } @@ -615,50 +721,21 @@ impl CronRegisterJob { unsafe { Self::spawn_bootout(this) }; } - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. - #[cfg(target_os = "macos")] - unsafe fn spawn_bootout(this: *mut Self) { - // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - s.state = RegisterState::BootingOut; - let uid_str = match alloc_print_z(format_args!( - "gui/{}/bun.cron.{}", - get_uid(), - bstr::BStr::new(s.title.as_bytes()) - )) { - Ok(v) => v, - Err(_) => { - s.set_err(format_args!("Out of memory")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - }; - let mut argv: [*const c_char; 4] = [ - c"/bin/launchctl".as_ptr().cast(), - c"bootout".as_ptr().cast(), - uid_str.as_ptr().cast(), - core::ptr::null(), - ]; - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Ignore) }; - drop(uid_str); - } - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. #[cfg(target_os = "macos")] unsafe fn spawn_bootstrap(this: *mut Self) { // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. let s = unsafe { &mut *this }; - s.state = RegisterState::Bootstrapping; - let Some(plist_path) = s.tmp_path.take() else { - s.set_err(format_args!("No plist path")); + s.base.state = CronJobState::Bootstrapping; + let Some(plist_path) = s.base.tmp_path.take() else { + s.base.set_err(format_args!("No plist path")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; }; let uid_str = match alloc_print_z(format_args!("gui/{}", get_uid())) { Ok(v) => v, Err(_) => { - s.set_err(format_args!("Out of memory")); + s.base.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } @@ -778,35 +855,15 @@ pub fn cron_register(global: &JSGlobalObject, frame: &CallFrame) -> JsResult(global, title_slice.slice()), bun_exe, abs_path, schedule: ZString::from_bytes(normalized_schedule), - title: ZString::from_bytes(title_slice.slice()), #[cfg(windows)] parsed_cron: parsed, - state: RegisterState::ReadingCrontab, - process: None, - stdout_reader: OutputReader::init::(), - #[cfg(windows)] - stderr_reader: OutputReader::init::(), - remaining_fds: 0, - has_called_process_exit: false, - exit_status: None, - err_msg: None, - tmp_path: None, - // SAFETY: `vm_mut().event_loop()` returns the live per-thread `jsc::EventLoop`. - event_loop_handle: EventLoopHandle::init(vm_mut().event_loop().cast::<()>()), })); - let promise_value = { - // SAFETY: just allocated; unique. Short-lived borrow ends before - // `start_*` (which may free `job`). - let job_ref = unsafe { &mut *job }; - job_ref.poll.ref_(bun_io::js_vm_ctx()); - job_ref.promise.value() - }; + // SAFETY: `job` is the freshly-leaked Box; unique until `start_*` runs. + let promise_value = unsafe { arm_job(job) }; // SAFETY: `job` is the freshly-leaked Box; `start_*` consumes it on // synchronous failure or hands it to the event loop on success. @@ -835,15 +892,15 @@ impl CronRegisterJob { unsafe fn start_windows(this: *mut Self) { // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. let s = unsafe { &mut *this }; - s.state = RegisterState::InstallingCrontab; + s.base.state = CronJobState::InstallingCrontab; let task_name = match alloc_print_z(format_args!( "bun-cron-{}", - bstr::BStr::new(s.title.as_bytes()) + bstr::BStr::new(s.base.title.as_bytes()) )) { Ok(v) => v, Err(_) => { - s.set_err(format_args!("Out of memory")); + s.base.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } @@ -852,18 +909,18 @@ impl CronRegisterJob { let xml = match cron_to_task_xml( &s.parsed_cron, s.bun_exe.as_bytes(), - s.title.as_bytes(), + s.base.title.as_bytes(), s.schedule.as_bytes(), s.abs_path.as_bytes(), ) { Ok(x) => x, Err(e) => { if e == TaskXmlError::TooManyTriggers { - s.set_err(format_args!( + s.base.set_err(format_args!( "This cron expression requires too many triggers for Windows Task Scheduler (max 48). Simplify the expression or use fewer restricted fields." )); } else { - s.set_err(format_args!("Failed to build task XML")); + s.base.set_err(format_args!("Failed to build task XML")); } // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; @@ -873,30 +930,32 @@ impl CronRegisterJob { let xml_path = match make_temp_path("bun-cron-xml-") { Ok(p) => p, Err(_) => { - s.set_err(format_args!("Out of memory")); + s.base.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } }; let xml_path_ptr = xml_path.as_ptr(); - s.tmp_path = Some(xml_path); + s.base.tmp_path = Some(xml_path); let file = match File::openat( Fd::cwd(), - s.tmp_path.as_ref().unwrap(), + s.base.tmp_path.as_ref().unwrap(), sys::O::WRONLY | sys::O::CREAT | sys::O::EXCL, 0o600, ) { Ok(f) => f, Err(_) => { - s.set_err(format_args!("Failed to create temp XML file")); + s.base + .set_err(format_args!("Failed to create temp XML file")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } }; if file.write_all(&xml).is_err() { let _ = file.close(); // close error is non-actionable - s.set_err(format_args!("Failed to write temp XML file")); + s.base + .set_err(format_args!("Failed to write temp XML file")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } @@ -910,29 +969,12 @@ impl CronRegisterJob { b"/tn\0".as_ptr().cast(), task_name.as_ptr().cast(), b"/np\0".as_ptr().cast(), - b"/f\0".as_ptr().cast(), - core::ptr::null(), - ]; - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Ignore) }; - drop(task_name); - } -} - -impl Drop for CronRegisterJob { - fn drop(&mut self) { - // stdout_reader / stderr_reader drop via their own Drop. - if let Some(proc) = self.process.take() { - // SAFETY: intrusive-RC pointer; we hold a ref. - unsafe { - (*proc).detach(); - Process::deref(proc); - } - } - if let Some(p) = self.tmp_path.take() { - let _ = sys::unlink(&p); - } - // err_msg, abs_path, schedule, title freed via field Drop. + b"/f\0".as_ptr().cast(), + core::ptr::null(), + ]; + // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Ignore) }; + drop(task_name); } } @@ -944,36 +986,7 @@ const ASCII_WHITESPACE: [u8; 6] = *b" \t\n\r\x0b\x0c"; // ============================================================================ pub struct CronRemoveJob { - promise: jsc::JSPromiseStrong, - // LIFETIMES.tsv: JSC_BORROW → GlobalRef - global: GlobalRef, - poll: KeepAlive, - title: ZString, - - state: RemoveState, - // LIFETIMES.tsv: SHARED — `Process` is intrusively refcounted (`*mut`). - process: Option<*mut Process>, - stdout_reader: OutputReader, - #[cfg(windows)] - stderr_reader: OutputReader, - remaining_fds: i8, - has_called_process_exit: bool, - exit_status: Option, - err_msg: Option>, - tmp_path: Option, - /// Typed enum for the io-layer FilePoll vtable (`bun_io::EventLoopHandle` - /// wraps `*const EventLoopHandle`). - event_loop_handle: EventLoopHandle, -} - -#[repr(u8)] -#[derive(Clone, Copy, PartialEq, Eq)] -enum RemoveState { - ReadingCrontab, - InstallingCrontab, - BootingOut, - Done, - Failed, + base: CronJobCommon, } // Forward as raw ptr — `maybe_finished` (via `CronJobBase`) may free `this`. @@ -983,108 +996,25 @@ bun_io::impl_buffered_reader_parent! { on_reader_done = |this| ::on_reader_done(this); on_reader_error = |this, err| ::on_reader_error(this, err); loop_ = |this| ::loop_(&*this).cast(); - event_loop = |this| (*this).event_loop_handle.as_event_loop_ctx(); + event_loop = |this| (*this).base.event_loop_handle.as_event_loop_ctx(); } impl CronJobBase for CronRemoveJob { - fn remaining_fds_mut(&mut self) -> &mut i8 { - &mut self.remaining_fds - } - fn err_msg_mut(&mut self) -> &mut Option> { - &mut self.err_msg - } - fn has_called_process_exit_mut(&mut self) -> &mut bool { - &mut self.has_called_process_exit - } - fn exit_status_mut(&mut self) -> &mut Option { - &mut self.exit_status - } - unsafe fn maybe_finished(this: *mut Self) { - // SAFETY: caller guarantees `this` is the live heap job with no active borrows. - unsafe { CronRemoveJob::maybe_finished(this) } + const EXIT_KIND: bun_spawn::ProcessExitKind = bun_spawn::ProcessExitKind::CronRemove; + fn base(&self) -> &CronJobCommon { + &self.base } -} - -impl CronRemoveJob { - fn set_err(&mut self, args: core::fmt::Arguments<'_>) { - if self.err_msg.is_none() { - let mut msg = Vec::new(); - let _ = msg.write_fmt(args); - self.err_msg = Some(msg); - } + fn base_mut(&mut self) -> &mut CronJobCommon { + &mut self.base } - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. - unsafe fn maybe_finished(this: *mut Self) { - // SAFETY: local reborrow (no FnEntry protector); not used after any - // call below that may free `this`. - let s = unsafe { &mut *this }; - if !s.has_called_process_exit || s.remaining_fds != 0 { - return; - } - if let Some(proc) = s.process.take() { - // SAFETY: intrusive-RC pointer; we hold a ref. - unsafe { - (*proc).detach(); - Process::deref(proc); - } - } - if s.err_msg.is_some() { - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - let Some(status) = s.exit_status.take() else { - return; - }; - match status { - Status::Exited(exited) => { - let is_acceptable_nonzero = (s.state == RemoveState::ReadingCrontab - && exited.code == 1) - || s.state == RemoveState::BootingOut - // On Windows, schtasks /delete exits non-zero when the task doesn't exist; - // removal of a non-existent job should resolve without error. - || (cfg!(windows) && s.state == RemoveState::InstallingCrontab); - if exited.code != 0 && !is_acceptable_nonzero { - // Owned copy: `final_buffer()` is `&mut self` and would - // alias `s.set_err` below. Copy the trimmed bytes out. - #[cfg(windows)] - let stderr_owned: Vec = bun_core::immutable::trim( - s.stderr_reader.final_buffer().as_slice(), - &ASCII_WHITESPACE, - ) - .to_vec(); - #[cfg(windows)] - let stderr_output: &[u8] = stderr_owned.as_slice(); - #[cfg(not(windows))] - let stderr_output: &[u8] = b""; - if !stderr_output.is_empty() { - s.set_err(format_args!("{}", bstr::BStr::new(stderr_output))); - } else { - s.set_err(format_args!("Process exited with code {}", exited.code)); - } - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - } - Status::Signaled(sig) => { - if s.state != RemoveState::BootingOut { - s.set_err(format_args!("Process killed by signal {}", sig as i32)); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - } - Status::Err(err) => { - s.set_err(format_args!( - "Process error: {}", - <&'static str>::from(err.get_errno()) - )); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - Status::Running => return, - } - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::advance_state(this) }; + fn accepts_nonzero_exit(&self, code: u8) -> bool { + let state = self.base.state; + (state == CronJobState::ReadingCrontab && code == 1) + || state == CronJobState::BootingOut + // On Windows, schtasks /delete exits non-zero when the task doesn't exist; + // removal of a non-existent job should resolve without error. + || (cfg!(windows) && state == CronJobState::InstallingCrontab) } /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. @@ -1093,21 +1023,21 @@ impl CronRemoveJob { let s = unsafe { &mut *this }; #[cfg(target_os = "macos")] { - match s.state { - RemoveState::BootingOut => { + match s.base.state { + CronJobState::BootingOut => { let Some(home) = env_var::HOME.get() else { - s.set_err(format_args!("HOME not set")); + s.base.set_err(format_args!("HOME not set")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; }; if let Ok(plist_path) = alloc_print_z(format_args!( "{}/Library/LaunchAgents/bun.cron.{}.plist", bstr::BStr::new(home), - bstr::BStr::new(s.title.as_bytes()) + bstr::BStr::new(s.base.title.as_bytes()) )) { let _ = sys::unlink(&plist_path); } else { - s.set_err(format_args!("Out of memory")); + s.base.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } @@ -1115,7 +1045,7 @@ impl CronRemoveJob { unsafe { Self::finish(this) }; } _ => { - s.set_err(format_args!("Unexpected state")); + s.base.set_err(format_args!("Unexpected state")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. unsafe { Self::finish(this) }; } @@ -1123,166 +1053,31 @@ impl CronRemoveJob { } #[cfg(not(target_os = "macos"))] { - match s.state { + match s.base.state { // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - RemoveState::ReadingCrontab => unsafe { Self::remove_crontab_entry(this) }, + CronJobState::ReadingCrontab => unsafe { Self::remove_crontab_entry(this) }, // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - RemoveState::InstallingCrontab => unsafe { Self::finish(this) }, + CronJobState::InstallingCrontab => unsafe { Self::finish(this) }, _ => { - s.set_err(format_args!("Unexpected state")); + s.base.set_err(format_args!("Unexpected state")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. unsafe { Self::finish(this) }; } } } } +} - /// Consumes and frees `this` (`heap::take`). - unsafe fn finish(this: *mut Self) { - // SAFETY: caller holds the unique Box; consumed below. Local - // reborrow has no FnEntry protector and is not used after the drop. - let this_ref = unsafe { &mut *this }; - this_ref.state = if this_ref.err_msg.is_some() { - RemoveState::Failed - } else { - RemoveState::Done - }; - this_ref.poll.unref(bun_io::js_vm_ctx()); - let ev = VirtualMachine::get().event_loop_mut(); - ev.enter(); - if let Some(msg) = &this_ref.err_msg { - let _ = this_ref.promise.reject_with_async_stack( - &this_ref.global, - Ok(this_ref - .global - .create_error_instance(format_args!("{}", bstr::BStr::new(msg)))), - ); - } else { - let _ = this_ref - .promise - .resolve(&this_ref.global, JSValue::UNDEFINED); - } - // Drop runs INSIDE the enter/exit scope so Process detach/deref and - // reader teardown observe the entered event-loop state. - // SAFETY: `this` was created via heap::alloc in cron_remove. - unsafe { drop(bun_core::heap::take(this)) }; - ev.exit(); - } - - /// May free `this` (via spawn → synchronous exit → finish, or error path). - unsafe fn spawn_cmd( - this: *mut Self, - argv: &mut [*const c_char], - stdin_opt: spawn::Stdio, - stdout_opt: spawn::Stdio, - ) { - // SAFETY: `this` is the live heap job (caller contract); may be freed inside. - unsafe { spawn_cmd_generic(this, argv, stdin_opt, stdout_opt) }; - } - - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. - #[cfg(all(not(target_os = "macos"), not(windows)))] - unsafe fn start_linux(this: *mut Self) { - // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - s.state = RemoveState::ReadingCrontab; - s.stdout_reader = OutputReader::init::(); - s.stdout_reader.set_parent(this.cast()); - let Some(crontab_path) = find_crontab() else { - s.set_err(format_args!("crontab not found in PATH")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - }; - let mut argv: [*const c_char; 3] = [crontab_path, c"-l".as_ptr(), core::ptr::null()]; - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Buffer) }; - } - +impl CronRemoveJob { /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. #[cfg(not(target_os = "macos"))] unsafe fn remove_crontab_entry(this: *mut Self) { - // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - let existing_content = s.stdout_reader.final_buffer().as_slice(); - let mut result: Vec = Vec::new(); - - if filter_crontab(existing_content, s.title.as_bytes(), &mut result).is_err() { - s.set_err(format_args!("Out of memory")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - - let tmp_path = match make_temp_path("bun-cron-rm-") { - Ok(p) => p, - Err(_) => { - s.set_err(format_args!("Out of memory")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - }; - let tmp_path_ptr = tmp_path.as_ptr(); - s.tmp_path = Some(tmp_path); - - let file = match File::openat( - Fd::cwd(), - s.tmp_path.as_ref().unwrap(), - sys::O::WRONLY | sys::O::CREAT | sys::O::EXCL, - 0o600, - ) { - Ok(f) => f, - Err(_) => { - s.set_err(format_args!("Failed to create temp file")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - }; - if file.write_all(&result).is_err() { - let _ = file.close(); // close error is non-actionable - s.set_err(format_args!("Failed to write temp file")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } - let _ = file.close(); // close error is non-actionable - - s.state = RemoveState::InstallingCrontab; - s.stdout_reader = OutputReader::init::(); - let Some(crontab_path) = find_crontab() else { - s.set_err(format_args!("crontab not found in PATH")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - }; - let mut argv: [*const c_char; 3] = [crontab_path, tmp_path_ptr.cast(), core::ptr::null()]; - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Ignore) }; - } - - /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. - #[cfg(target_os = "macos")] - unsafe fn start_mac(this: *mut Self) { - // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - s.state = RemoveState::BootingOut; - let uid_str = match alloc_print_z(format_args!( - "gui/{}/bun.cron.{}", - get_uid(), - bstr::BStr::new(s.title.as_bytes()) - )) { - Ok(v) => v, - Err(_) => { - s.set_err(format_args!("Out of memory")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; - } + // SAFETY: `this` is the live heap job; freed inside on failure. + let Some(result) = (unsafe { Self::take_filtered_crontab(this) }) else { + return; }; - let mut argv: [*const c_char; 4] = [ - c"/bin/launchctl".as_ptr().cast(), - c"bootout".as_ptr().cast(), - uid_str.as_ptr().cast(), - core::ptr::null(), - ]; - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::spawn_cmd(this, &mut argv, spawn::Stdio::Ignore, spawn::Stdio::Ignore) }; - drop(uid_str); + // SAFETY: `this` is the live heap job; `install_crontab` may free it. + unsafe { Self::install_crontab(this, &result, "bun-cron-rm-") }; } } @@ -1305,35 +1100,15 @@ pub fn cron_remove(global: &JSGlobalObject, frame: &CallFrame) -> JsResult(), - #[cfg(windows)] - stderr_reader: OutputReader::init::(), - remaining_fds: 0, - has_called_process_exit: false, - exit_status: None, - err_msg: None, - tmp_path: None, - // SAFETY: `vm_mut().event_loop()` returns the live per-thread `jsc::EventLoop`. - event_loop_handle: EventLoopHandle::init(vm_mut().event_loop().cast::<()>()), + base: CronJobCommon::init::(global, title_slice.slice()), })); - let promise_value = { - // SAFETY: just allocated; unique. Short-lived borrow ends before - // `start_*` (which may free `job`). - let job_ref = unsafe { &mut *job }; - job_ref.poll.ref_(bun_io::js_vm_ctx()); - job_ref.promise.value() - }; + // SAFETY: `job` is the freshly-leaked Box; unique until `start_*` runs. + let promise_value = unsafe { arm_job(job) }; // SAFETY: `job` is the freshly-leaked Box; `start_*` consumes it on // synchronous failure or hands it to the event loop on success. #[cfg(target_os = "macos")] unsafe { - CronRemoveJob::start_mac(job) + CronRemoveJob::spawn_bootout(job) }; #[cfg(windows)] unsafe { @@ -1353,14 +1128,14 @@ impl CronRemoveJob { unsafe fn start_windows(this: *mut Self) { // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. let s = unsafe { &mut *this }; - s.state = RemoveState::InstallingCrontab; + s.base.state = CronJobState::InstallingCrontab; let task_name = match alloc_print_z(format_args!( "bun-cron-{}", - bstr::BStr::new(s.title.as_bytes()) + bstr::BStr::new(s.base.title.as_bytes()) )) { Ok(v) => v, Err(_) => { - s.set_err(format_args!("Out of memory")); + s.base.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { Self::finish(this) }; } @@ -1379,21 +1154,6 @@ impl CronRemoveJob { } } -impl Drop for CronRemoveJob { - fn drop(&mut self) { - if let Some(proc) = self.process.take() { - // SAFETY: intrusive-RC pointer; we hold a ref. - unsafe { - (*proc).detach(); - Process::deref(proc); - } - } - if let Some(p) = self.tmp_path.take() { - let _ = sys::unlink(&p); - } - } -} - // ============================================================================ // CronJob — in-process callback-style cron (Bun.cron(expr, cb)) // ============================================================================ @@ -2061,20 +1821,6 @@ pub fn cron_parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult); - /// Consumes and frees `this`. - unsafe fn finish(this: *mut Self); - fn process_slot(&mut self) -> &mut Option<*mut Process>; - #[cfg(unix)] - fn stdout_reader(&mut self) -> &mut OutputReader; - #[cfg(windows)] - fn stderr_reader(&mut self) -> &mut OutputReader; - fn remaining_fds(&mut self) -> &mut i8; -} - bun_spawn::link_impl_ProcessExit! { CronRegister for CronRegisterJob => |this| { // Forward `this` raw — `on_process_exit` → `maybe_finished` may free it. @@ -2089,73 +1835,24 @@ bun_spawn::link_impl_ProcessExit! { } } -impl SpawnCmdTarget for CronRegisterJob { - const EXIT_KIND: bun_spawn::ProcessExitKind = bun_spawn::ProcessExitKind::CronRegister; - fn set_err(&mut self, args: core::fmt::Arguments<'_>) { - CronRegisterJob::set_err(self, args) - } - unsafe fn finish(this: *mut Self) { - // SAFETY: caller guarantees `this` is the live heap job with no active borrows. - unsafe { CronRegisterJob::finish(this) } - } - fn process_slot(&mut self) -> &mut Option<*mut Process> { - &mut self.process - } - #[cfg(unix)] - fn stdout_reader(&mut self) -> &mut OutputReader { - &mut self.stdout_reader - } - #[cfg(windows)] - fn stderr_reader(&mut self) -> &mut OutputReader { - &mut self.stderr_reader - } - fn remaining_fds(&mut self) -> &mut i8 { - &mut self.remaining_fds - } -} -impl SpawnCmdTarget for CronRemoveJob { - const EXIT_KIND: bun_spawn::ProcessExitKind = bun_spawn::ProcessExitKind::CronRemove; - fn set_err(&mut self, args: core::fmt::Arguments<'_>) { - CronRemoveJob::set_err(self, args) - } - unsafe fn finish(this: *mut Self) { - // SAFETY: caller guarantees `this` is the live heap job with no active borrows. - unsafe { CronRemoveJob::finish(this) } - } - fn process_slot(&mut self) -> &mut Option<*mut Process> { - &mut self.process - } - #[cfg(unix)] - fn stdout_reader(&mut self) -> &mut OutputReader { - &mut self.stdout_reader - } - #[cfg(windows)] - fn stderr_reader(&mut self) -> &mut OutputReader { - &mut self.stderr_reader - } - fn remaining_fds(&mut self) -> &mut i8 { - &mut self.remaining_fds - } -} - /// Generic spawn used by both CronRegisterJob and CronRemoveJob. /// /// May free `this` (synchronously, via either an early `T::finish` on setup /// error or `watch_or_reap` → exit handler → `maybe_finished` → `finish`). /// Raw-ptr receiver: see [`CronJobBase`] note. Callers must not touch /// `this` after this returns. -unsafe fn spawn_cmd_generic( +unsafe fn spawn_cmd_generic( this: *mut T, argv: &mut [*const c_char], stdin_opt: spawn::Stdio, stdout_opt: spawn::Stdio, ) { - // SAFETY: local reborrow (no FnEntry protector). Re-derived after each - // section so no `&mut T` outlives a potentially-freeing call. - let s = unsafe { &mut *this }; - *s.has_called_process_exit_mut() = false; - *s.exit_status_mut() = None; - *s.remaining_fds() = 0; + // SAFETY: local reborrow (no FnEntry protector); last use precedes any + // potentially-freeing call. + let b = unsafe { &mut *this }.base_mut(); + b.has_called_process_exit = false; + b.exit_status = None; + b.remaining_fds = 0; #[cfg(not(windows))] let resolved_argv0: Option<*const c_char> = None; @@ -2176,7 +1873,7 @@ unsafe fn spawn_cmd_generic( match bun_which::which(&mut path_buf, path_env, b"", argv0) { Some(p) => resolved_argv0 = Some(p.as_ptr().cast()), None => { - s.set_err(format_args!( + b.set_err(format_args!( "Could not find '{}' in PATH", bstr::BStr::new(argv0) )); @@ -2203,7 +1900,7 @@ unsafe fn spawn_cmd_generic( envp_owned.as_ptr().cast() } Err(_) => { - s.set_err(format_args!("Failed to create environment block")); + b.set_err(format_args!("Failed to create environment block")); return unsafe { T::finish(this) }; } } @@ -2263,7 +1960,7 @@ unsafe fn spawn_cmd_generic( // `Drop`. Reclaim it (uv_close + free if init'd) here. #[cfg(windows)] spawn_options.stderr.deinit(); - s.set_err(format_args!( + b.set_err(format_args!( "Failed to spawn process: {}", bstr::BStr::new(err.name()) )); @@ -2273,7 +1970,7 @@ unsafe fn spawn_cmd_generic( Err(e) => { #[cfg(windows)] spawn_options.stderr.deinit(); - s.set_err(format_args!("Failed to spawn process: {}", e.name())); + b.set_err(format_args!("Failed to spawn process: {}", e.name())); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { T::finish(this) }; } @@ -2286,12 +1983,12 @@ unsafe fn spawn_cmd_generic( if let Some(stdout) = spawned.stdout { let this_ptr = this.cast::(); if !spawned.memfds[1] { - s.stdout_reader().set_parent(this_ptr); + b.stdout_reader.set_parent(this_ptr); let _ = sys::set_nonblocking(stdout); - *s.remaining_fds() += 1; + b.remaining_fds += 1; { use bun_io::pipe_reader::PosixFlags; - let flags = &mut s.stdout_reader().flags; + let flags = &mut b.stdout_reader.flags; flags.insert(PosixFlags::NONBLOCKING | PosixFlags::SOCKET); flags.remove( PosixFlags::MEMFD @@ -2299,17 +1996,17 @@ unsafe fn spawn_cmd_generic( | PosixFlags::CLOSED_WITHOUT_REPORTING, ); } - if s.stdout_reader().start(stdout, true).is_err() { - s.set_err(format_args!("Failed to start reading stdout")); + if b.stdout_reader.start(stdout, true).is_err() { + b.set_err(format_args!("Failed to start reading stdout")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. return unsafe { T::finish(this) }; } - if let Some(p) = s.stdout_reader().handle.get_poll() { + if let Some(p) = b.stdout_reader.handle.get_poll() { p.set_flag(bun_io::FilePollFlag::Socket); } } else { - s.stdout_reader().set_parent(this_ptr); - s.stdout_reader().start_memfd(stdout); + b.stdout_reader.set_parent(this_ptr); + b.stdout_reader.start_memfd(stdout); } } } @@ -2326,12 +2023,11 @@ unsafe fn spawn_cmd_generic( // callback + double-free on reader close). if let spawn::WindowsStdioResult::Buffer(pipe) = spawned.stderr.take() { debug_assert!(core::ptr::eq(Box::as_ref(&pipe), stderr_pipe_ptr)); - s.stderr_reader().source = Some(bun_io::Source::Pipe(pipe)); - s.stderr_reader() - .set_parent(this.cast::()); - *s.remaining_fds() += 1; - if s.stderr_reader().start_with_current_pipe().is_err() { - s.set_err(format_args!("Failed to start reading stderr")); + b.stderr_reader.source = Some(bun_io::Source::Pipe(pipe)); + b.stderr_reader.set_parent(this.cast::()); + b.remaining_fds += 1; + if b.stderr_reader.start_with_current_pipe().is_err() { + b.set_err(format_args!("Failed to start reading stderr")); return unsafe { T::finish(this) }; } } @@ -2340,18 +2036,18 @@ unsafe fn spawn_cmd_generic( // SAFETY: `vm_mut().event_loop()` returns the live per-thread `jsc::EventLoop`. let ev_handle = EventLoopHandle::init(vm_mut().event_loop().cast::<()>()); let process = spawned.to_process(ev_handle, false); - *s.process_slot() = Some(process); + b.process = Some(process); // SAFETY: `process` was just allocated by `to_process`; we hold the only // ref. `this` is the owning `Box` (only freed in `T::finish`, gated on // `has_called_process_exit`), so it outlives `process`. unsafe { (*process).set_exit_handler(bun_spawn::ProcessExit::new(T::EXIT_KIND, this)) }; - // `s` not used past this point — `watch_or_reap` may synchronously invoke + // `b` not used past this point — `watch_or_reap` may synchronously invoke // the exit handler, which can free `this`. // SAFETY: `process` is live; `watch_or_reap` may synchronously invoke the // exit handler (which re-enters `this` via the vtable thunk). match unsafe { (*process).watch_or_reap() } { Err(err) => { - // SAFETY: we hold a ref on `process` via `process_slot()`; it is live. + // SAFETY: we hold a ref on `process` via the base's `process` slot; it is live. if !unsafe { (*process).has_exited() } { // SAFETY: all-zero is a valid Rusage. let rusage = bun_core::ffi::zeroed::(); diff --git a/src/runtime/api/csrf_jsc.rs b/src/runtime/api/csrf_jsc.rs index 7629a69fc597..7a70f9b087e9 100644 --- a/src/runtime/api/csrf_jsc.rs +++ b/src/runtime/api/csrf_jsc.rs @@ -71,6 +71,67 @@ fn get_optional_int_u64( Ok(Some(num as u64)) } +/// Reads an optional string option that must be non-empty when present. +/// `label` is the user-facing name used in the error message. +fn parse_non_empty_opt( + options: JSValue, + global: &JSGlobalObject, + property: &'static [u8], + label: &str, +) -> JsResult> { + match get_optional_slice(options, global, property)? { + Some(slice) if slice.slice().is_empty() => { + Err(global.throw_invalid_arguments(format_args!("{label} must be a non-empty string"))) + } + other => Ok(other), + } +} + +/// Reads the optional `encoding` option. Returns `None` when absent. +fn parse_encoding_opt( + options: JSValue, + global: &JSGlobalObject, +) -> JsResult> { + let Some(encoding_js) = options.get(global, "encoding")? else { + return Ok(None); + }; + let encoding_enum = + NodeEncoding::from_js_with_default_on_empty(encoding_js, global, NodeEncoding::Base64url)?; + match encoding_enum { + Some(NodeEncoding::Base64) => Ok(Some(csrf::TokenFormat::Base64)), + Some(NodeEncoding::Base64url) => Ok(Some(csrf::TokenFormat::Base64Url)), + Some(NodeEncoding::Hex) => Ok(Some(csrf::TokenFormat::Hex)), + _ => Err(global.throw_invalid_arguments(format_args!( + "Invalid format: must be 'base64', 'base64url', or 'hex'" + ))), + } +} + +/// Reads the optional `algorithm` option, restricted to the algorithms CSRF +/// supports. Returns `None` when absent. +fn parse_algorithm_opt( + options: JSValue, + global: &JSGlobalObject, +) -> JsResult> { + let Some(algorithm_js) = options.get(global, "algorithm")? else { + return Ok(None); + }; + if !algorithm_js.is_string() { + return Err(global.throw_invalid_argument_type_value("algorithm", "string", algorithm_js)); + } + match algorithm_from_js_case_insensitive(global, algorithm_js)? { + Some( + algo @ (EvpAlgorithm::Blake2b256 + | EvpAlgorithm::Blake2b512 + | EvpAlgorithm::Sha256 + | EvpAlgorithm::Sha384 + | EvpAlgorithm::Sha512 + | EvpAlgorithm::Sha512_256), + ) => Ok(Some(algo)), + _ => Err(global.throw_invalid_arguments(format_args!("Algorithm not supported"))), + } +} + /// JS binding function for generating CSRF tokens /// First argument is secret (required), second is options (optional) #[bun_jsc::host_fn] @@ -109,64 +170,16 @@ pub(crate) fn csrf__generate(global: &JSGlobalObject, frame: &CallFrame) -> JsRe } // Extract sessionId (optional) - if let Some(session_id_slice) = get_optional_slice(options_value, global, b"sessionId")? { - if session_id_slice.slice().is_empty() { - return Err(global.throw_invalid_arguments(format_args!( - "sessionId must be a non-empty string" - ))); - } - session_id = Some(session_id_slice); - } + session_id = parse_non_empty_opt(options_value, global, b"sessionId", "sessionId")?; // Extract encoding (optional) - if let Some(encoding_js) = options_value.get(global, "encoding")? { - let Some(encoding_enum) = NodeEncoding::from_js_with_default_on_empty( - encoding_js, - global, - NodeEncoding::Base64url, - )? - else { - return Err(global.throw_invalid_arguments(format_args!( - "Invalid format: must be 'base64', 'base64url', or 'hex'" - ))); - }; - encoding = match encoding_enum { - NodeEncoding::Base64 => csrf::TokenFormat::Base64, - NodeEncoding::Base64url => csrf::TokenFormat::Base64Url, - NodeEncoding::Hex => csrf::TokenFormat::Hex, - _ => { - return Err(global.throw_invalid_arguments(format_args!( - "Invalid format: must be 'base64', 'base64url', or 'hex'" - ))); - } - }; + if let Some(encoding_opt) = parse_encoding_opt(options_value, global)? { + encoding = encoding_opt; } - if let Some(algorithm_js) = options_value.get(global, "algorithm")? { - if !algorithm_js.is_string() { - return Err(global.throw_invalid_argument_type_value( - "algorithm", - "string", - algorithm_js, - )); - } - let Some(algo) = algorithm_from_js_case_insensitive(global, algorithm_js)? else { - return Err(global.throw_invalid_arguments(format_args!("Algorithm not supported"))); - }; + // Extract algorithm (optional) + if let Some(algo) = parse_algorithm_opt(options_value, global)? { algorithm = algo; - match algorithm { - EvpAlgorithm::Blake2b256 - | EvpAlgorithm::Blake2b512 - | EvpAlgorithm::Sha256 - | EvpAlgorithm::Sha384 - | EvpAlgorithm::Sha512 - | EvpAlgorithm::Sha512_256 => {} - _ => { - return Err( - global.throw_invalid_arguments(format_args!("Algorithm not supported")) - ); - } - } } } @@ -249,24 +262,11 @@ pub(crate) fn csrf__verify(global: &JSGlobalObject, frame: &CallFrame) -> JsResu if args.len() > 1 && args[1].is_object() { let options_value = args[1]; - // Extract the secret (required) - if let Some(secret_slice) = get_optional_slice(options_value, global, b"secret")? { - if secret_slice.slice().is_empty() { - return Err(global - .throw_invalid_arguments(format_args!("Secret must be a non-empty string"))); - } - secret = Some(secret_slice); - } + // Extract the secret (optional; falls back to the per-VM default) + secret = parse_non_empty_opt(options_value, global, b"secret", "Secret")?; // Extract sessionId (optional) - if let Some(session_id_slice) = get_optional_slice(options_value, global, b"sessionId")? { - if session_id_slice.slice().is_empty() { - return Err(global.throw_invalid_arguments(format_args!( - "sessionId must be a non-empty string" - ))); - } - session_id = Some(session_id_slice); - } + session_id = parse_non_empty_opt(options_value, global, b"sessionId", "sessionId")?; // Extract maxAge (optional) if let Some(max_age_js) = get_optional_int_u64(options_value, global, "maxAge")? { @@ -274,53 +274,13 @@ pub(crate) fn csrf__verify(global: &JSGlobalObject, frame: &CallFrame) -> JsResu } // Extract encoding (optional) - if let Some(encoding_js) = options_value.get(global, "encoding")? { - let Some(encoding_enum) = NodeEncoding::from_js_with_default_on_empty( - encoding_js, - global, - NodeEncoding::Base64url, - )? - else { - return Err(global.throw_invalid_arguments(format_args!( - "Invalid format: must be 'base64', 'base64url', or 'hex'" - ))); - }; - encoding = match encoding_enum { - NodeEncoding::Base64 => csrf::TokenFormat::Base64, - NodeEncoding::Base64url => csrf::TokenFormat::Base64Url, - NodeEncoding::Hex => csrf::TokenFormat::Hex, - _ => { - return Err(global.throw_invalid_arguments(format_args!( - "Invalid format: must be 'base64', 'base64url', or 'hex'" - ))); - } - }; + if let Some(encoding_opt) = parse_encoding_opt(options_value, global)? { + encoding = encoding_opt; } - if let Some(algorithm_js) = options_value.get(global, "algorithm")? { - if !algorithm_js.is_string() { - return Err(global.throw_invalid_argument_type_value( - "algorithm", - "string", - algorithm_js, - )); - } - let Some(algo) = algorithm_from_js_case_insensitive(global, algorithm_js)? else { - return Err(global.throw_invalid_arguments(format_args!("Algorithm not supported"))); - }; + + // Extract algorithm (optional) + if let Some(algo) = parse_algorithm_opt(options_value, global)? { algorithm = algo; - match algorithm { - EvpAlgorithm::Blake2b256 - | EvpAlgorithm::Blake2b512 - | EvpAlgorithm::Sha256 - | EvpAlgorithm::Sha384 - | EvpAlgorithm::Sha512 - | EvpAlgorithm::Sha512_256 => {} - _ => { - return Err( - global.throw_invalid_arguments(format_args!("Algorithm not supported")) - ); - } - } } } // Verify the token diff --git a/src/runtime/bake/DevServer.rs b/src/runtime/bake/DevServer.rs index 930ca31e7073..023bf36d1a46 100644 --- a/src/runtime/bake/DevServer.rs +++ b/src/runtime/bake/DevServer.rs @@ -449,6 +449,67 @@ pub struct DevServer { bun_event_loop::impl_timer_owner!(DevServer; from_timer_ptr => memory_visualizer_timer); +/// Exhaustiveness check: destructures a `&DevServer` without `..`, so adding, +/// removing, or renaming a field fails to compile at every invocation +/// (`Drop`, `memory_cost_detailed`), forcing the per-field logic there to be +/// reviewed. All bindings are `_` so nothing is moved or borrowed past the +/// statement. +macro_rules! destructure_dev_server_fields { + ($e:expr) => { + let crate::bake::dev_server::DevServer { + magic: _, + root: _, + inspector_server_id: _, + configuration_hash_key: _, + vm: _, + server: _, + router: _, + route_bundles: _, + graph_safety_lock: _, + client_graph: _, + server_graph: _, + barrel_files_with_deferrals: _, + barrel_needed_exports: _, + incremental_result: _, + route_lookup: _, + html_router: _, + assets: _, + source_maps: _, + bundling_failures: _, + frontend_only: _, + has_tailwind_plugin_hack: _, + server_fetch_function_callback: _, + server_register_update_callback: _, + bun_watcher: _, + directory_watchers: _, + watcher_atomics: _, + testing_batch_events: _, + generation: _, + bundles_since_last_error: _, + framework: _, + bundler_framework_views: _, + bundler_options: _, + server_transpiler: _, + client_transpiler: _, + ssr_transpiler: _, + log: _, + plugin_state: _, + current_bundle: _, + next_bundle: _, + deferred_request_pool: _, + active_websocket_connections: _, + dump_dir: _, + emit_incremental_visualizer_events: _, + emit_memory_visualizer_events: _, + memory_visualizer_timer: _, + has_pre_crash_handler: _, + assume_perfect_incremental_bundling: _, + broadcast_console_log_from_browser_to_server: _, + } = $e; + }; +} +pub(crate) use destructure_dev_server_fields; + pub(super) const INTERNAL_PREFIX: &str = "/_bun"; /// Assets which are routed to the `Assets` storage. pub(super) const ASSET_PREFIX: &str = const_format::concatcp!(INTERNAL_PREFIX, "/asset"); @@ -1070,62 +1131,11 @@ impl Drop for DevServer { // practice, so a plain fetch_add is fine. DEV_SERVER_DEINIT_COUNT_FOR_TESTING.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - // Exhaustiveness check: destructuring without `..` fails to compile when a field is added, - // removed, or renamed, forcing this Drop to be reviewed. All bindings - // are `_` so nothing is moved; cleanup not done explicitly below - // happens via the implicit field drops after this body returns. - { - let DevServer { - magic: _, - root: _, - inspector_server_id: _, - configuration_hash_key: _, - vm: _, - server: _, - router: _, - route_bundles: _, - graph_safety_lock: _, - client_graph: _, - server_graph: _, - barrel_files_with_deferrals: _, - barrel_needed_exports: _, - incremental_result: _, - route_lookup: _, - html_router: _, - assets: _, - source_maps: _, - bundling_failures: _, - frontend_only: _, - has_tailwind_plugin_hack: _, - server_fetch_function_callback: _, - server_register_update_callback: _, - bun_watcher: _, - directory_watchers: _, - watcher_atomics: _, - testing_batch_events: _, - generation: _, - bundles_since_last_error: _, - framework: _, - bundler_framework_views: _, - bundler_options: _, - server_transpiler: _, - client_transpiler: _, - ssr_transpiler: _, - log: _, - plugin_state: _, - current_bundle: _, - next_bundle: _, - deferred_request_pool: _, - active_websocket_connections: _, - dump_dir: _, - emit_incremental_visualizer_events: _, - emit_memory_visualizer_events: _, - memory_visualizer_timer: _, - has_pre_crash_handler: _, - assume_perfect_incremental_bundling: _, - broadcast_console_log_from_browser_to_server: _, - } = &*self; - } + // Exhaustiveness check (see `destructure_dev_server_fields!`): fails to + // compile when a field is added, removed, or renamed, forcing this + // Drop to be reviewed. Cleanup not done explicitly below happens via + // the implicit field drops after this body returns. + destructure_dev_server_fields!(&*self); // WebSockets should be deinitialized before other parts. // `websocket.close()` synchronously dispatches `HmrSocket.onClose`, diff --git a/src/runtime/bake/DevServer/memory_cost.rs b/src/runtime/bake/DevServer/memory_cost.rs index 30c0265a71e1..82cee7802b6a 100644 --- a/src/runtime/bake/DevServer/memory_cost.rs +++ b/src/runtime/bake/DevServer/memory_cost.rs @@ -33,62 +33,10 @@ pub(crate) fn memory_cost_detailed(dev: &DevServer) -> MemoryCost { let mut source_maps: usize = 0; let mut assets: usize = 0; - // Exhaustiveness check: - // destructuring without `..` fails to compile when a DevServer field is - // added, removed, or renamed, forcing the accounting below to be updated. - // All bindings are `_` so nothing is moved or borrowed past this block. - { - let DevServer { - magic: _, - root: _, - inspector_server_id: _, - configuration_hash_key: _, - vm: _, - server: _, - router: _, - route_bundles: _, - graph_safety_lock: _, - client_graph: _, - server_graph: _, - barrel_files_with_deferrals: _, - barrel_needed_exports: _, - incremental_result: _, - route_lookup: _, - html_router: _, - assets: _, - source_maps: _, - bundling_failures: _, - frontend_only: _, - has_tailwind_plugin_hack: _, - server_fetch_function_callback: _, - server_register_update_callback: _, - bun_watcher: _, - directory_watchers: _, - watcher_atomics: _, - testing_batch_events: _, - generation: _, - bundles_since_last_error: _, - framework: _, - bundler_framework_views: _, - bundler_options: _, - server_transpiler: _, - client_transpiler: _, - ssr_transpiler: _, - log: _, - plugin_state: _, - current_bundle: _, - next_bundle: _, - deferred_request_pool: _, - active_websocket_connections: _, - dump_dir: _, - emit_incremental_visualizer_events: _, - emit_memory_visualizer_events: _, - memory_visualizer_timer: _, - has_pre_crash_handler: _, - assume_perfect_incremental_bundling: _, - broadcast_console_log_from_browser_to_server: _, - } = dev; - } + // Exhaustiveness check (see `destructure_dev_server_fields!`): fails to + // compile when a DevServer field is added, removed, or renamed, forcing + // the accounting below to be updated. + crate::bake::dev_server_body::destructure_dev_server_fields!(dev); // does not contain pointers // .assume_perfect_incremental_bundling diff --git a/src/runtime/bake/bake_body.rs b/src/runtime/bake/bake_body.rs index 8d28a1bc91da..7400e9d19527 100644 --- a/src/runtime/bake/bake_body.rs +++ b/src/runtime/bake/bake_body.rs @@ -24,11 +24,11 @@ use bun_paths::{self as paths, PathBuffer}; pub(crate) use crate::api::js_bundler::Plugin; use crate::api::js_bundler::js_bundler::PluginJscExt as _; -// Note: parent `mod.rs` already declares `dev_server` / `framework_router` -// as sibling modules of this file; pull them in instead of re-declaring (which -// would duplicate the module tree and fail on `framework_router` having no +// Note: parent `mod.rs` already declares `framework_router` as a sibling +// module of this file; pull it in instead of re-declaring (which would +// duplicate the module tree and fail on `framework_router` having no // matching filename). -use super::{dev_server, framework_router}; +use super::framework_router; // Note: `pub use dev_server as DevServer` / `framework_router as // FrameworkRouter` are already provided by the parent `mod.rs` (lines 349/369); @@ -1140,38 +1140,6 @@ impl Framework { ) } - pub fn init_transpiler<'a>( - &mut self, - arena: &'a Arena, - log: &mut bun_ast::Log, - mode: Mode, - renderer: Graph, - out: &mut core::mem::MaybeUninit>, - bundler_options: &BuildConfigSubset, - ) -> Result<(), bun_core::Error> { - let source_map: bun_bundler::options::SourceMapOption = match mode { - // Source maps must always be external, as DevServer special cases - // the linking and part of the generation of these. It also relies - // on source maps always being enabled. - Mode::Development => bun_bundler::options::SourceMapOption::External, - // TODO: follow user configuration - _ => bun_bundler::options::SourceMapOption::None, - }; - - self.init_transpiler_with_options( - arena, - log, - mode, - renderer, - out, - bundler_options, - source_map, - None, - None, - None, - ) - } - pub fn init_transpiler_with_options<'a>( &mut self, arena: &'a Arena, @@ -1185,149 +1153,27 @@ impl Framework { minify_syntax: Option, minify_identifiers: Option, ) -> Result<(), bun_core::Error> { - // `ASTMemoryAllocator::enter` returns an RAII `Scope` whose `Drop` - // runs `exit()` at end-of-fn. - let mut ast_memory_allocator = bun_ast::ASTMemoryAllocator::borrowing(arena); - let _ast_scope = ast_memory_allocator.enter(); - - // The caller (`DevServer::init`) hands us an uninitialized slot, so - // use `MaybeUninit::write` (no drop of prior bytes) then reborrow as - // `&mut Transpiler` for the field assignments below. - let out: &mut bun_bundler::Transpiler = out.write(bun_bundler::Transpiler::init( + // The arena slot for the `bake_types::Framework` projection is + // deliberately not tracked here (DevServer's keystone wrapper is the + // path that `drop_in_place`s it; see `init_transpiler_impl` docs). + super::init_transpiler_impl( arena, log, - // `TransformOptions::default()`: every `Option` is `None`, every - // slice empty, every scalar zero/false. - bun_schema::api::TransformOptions::default(), - None, - )?); - - out.options.target = match renderer { - Graph::Client => bun_ast::Target::Browser, - Graph::Server | Graph::Ssr => bun_ast::Target::Bun, - }; - out.options.public_path = match renderer { - Graph::Client => dev_server::CLIENT_PREFIX.as_bytes().into(), - Graph::Server | Graph::Ssr => Box::default(), - }; - out.options.entry_points = Box::default(); - out.options.log = log; - out.options.output_format = match mode { - Mode::Development => bun_bundler::options::Format::InternalBakeDev, - Mode::ProductionDynamic | Mode::ProductionStatic => bun_bundler::options::Format::Esm, - }; - out.options.out_extensions = bun_collections::StringHashMap::new(); - out.options.hot_module_reloading = mode == Mode::Development; - out.options.code_splitting = mode != Mode::Development; - - // force disable filesystem output, even though bundle_v2 - // is special cased to return before that code is reached. - out.options.output_dir = Box::default(); - - // framework configuration - out.options.react_fast_refresh = mode == Mode::Development - && renderer == Graph::Client - && self.react_fast_refresh.is_some(); - out.options.server_components = self.server_components.is_some(); - - out.options.conditions = bun_bundler::options::ESMConditions::init( - out.options.target.default_conditions(), - out.options.target.is_server_side(), - bundler_options.conditions.keys(), - )?; - if renderer == Graph::Server && self.server_components.is_some() { - out.options.conditions.append_slice(&[b"react-server"])?; - } - if mode == Mode::Development { - // Support `esm-env` package using this condition. - out.options.conditions.append_slice(&[b"development"])?; - } - // Ensure "node" condition is included for server-side rendering - // This helps with package.json imports field resolution - if renderer == Graph::Server || renderer == Graph::Ssr { - out.options.conditions.append_slice(&[b"node"])?; - } - - out.options.production = mode != Mode::Development; - out.options.tree_shaking = mode != Mode::Development; - out.options.minify_syntax = minify_syntax.unwrap_or(mode != Mode::Development); - out.options.minify_identifiers = minify_identifiers.unwrap_or(mode != Mode::Development); - out.options.minify_whitespace = minify_whitespace.unwrap_or(mode != Mode::Development); - out.options.css_chunking = true; - // The bundler crate (lower tier) carries a TYPE_ONLY projection - // (`bake_types::Framework`); construct it here and give it arena - // lifetime so `BundleOptions<'a>` can borrow it for the bundle pass. - // NOTE: interior `Box<[u8]>` in the projection are not dropped by - // bumpalo — bounded per-session, revisit when `bake_types::BuiltInModule` - // is reshaped to `&'a [u8]`. - out.options.framework = Some(&*arena.alloc(self.as_bundler_view())); - out.options.inline_entrypoint_import_meta_main = true; - if let Some(ignore) = bundler_options.ignore_dce_annotations { - out.options.ignore_dce_annotations = ignore; - } - - out.options.source_map = source_map; - if bundler_options.env != bun_schema::api::DotEnvBehavior::_none { - out.options.env.behavior = bundler_options.env; - out.options.env.prefix = bundler_options.env_prefix.unwrap_or(b"").into(); - } - // The resolver crate carries a FORWARD_DECL subset of - // `BundleOptions`, so re-project via the dedicated helper rather than - // `Clone`. - out.sync_resolver_opts(); - - out.configure_linker(); - out.configure_defines()?; - - out.options.jsx.development = mode == Mode::Development; - - add_import_meta_defines( - &mut out.options.define, mode, - match renderer { - Graph::Client => Side::Client, - Graph::Server | Graph::Ssr => Side::Server, + renderer, + out, + bundler_options, + super::InitTranspilerOptions { + source_map, + minify_whitespace, + minify_syntax, + minify_identifiers, + has_react_fast_refresh: self.react_fast_refresh.is_some(), + has_server_components: self.server_components.is_some(), + framework_view: self.as_bundler_view(), }, - )?; - - if (bundler_options.define.keys.len() + bundler_options.drop.count()) > 0 { - debug_assert_eq!( - bundler_options.define.keys.len(), - bundler_options.define.values.len() - ); - use bun_bundler::DefineDataExt; - for (k, v) in bundler_options - .define - .keys - .iter() - .zip(bundler_options.define.values.iter()) - { - let parsed = - bun_bundler::defines::DefineData::parse(k, v, false, false, log, arena)?; - out.options.define.insert(k, parsed)?; - } - - for drop_item in bundler_options.drop.keys() { - if !drop_item.is_empty() { - let parsed = bun_bundler::defines::DefineData::parse( - drop_item, b"", true, true, log, arena, - )?; - out.options.define.insert(drop_item, parsed)?; - } - } - } - - if mode != Mode::Development { - // Hide information about the source repository, at the cost of debugging quality. - out.options.entry_naming = b"_bun/[hash].[ext]".as_slice().into(); - out.options.chunk_naming = b"_bun/[hash].[ext]".as_slice().into(); - out.options.asset_naming = b"_bun/[hash].[ext]".as_slice().into(); - } - - // Re-sync after define/naming mutations so the resolver sees the - // final option set. - out.sync_resolver_opts(); - Ok(()) + ) + .map(|_framework_view| ()) } } diff --git a/src/runtime/bake/mod.rs b/src/runtime/bake/mod.rs index 39d4d9f35b97..35ca15c689e3 100644 --- a/src/runtime/bake/mod.rs +++ b/src/runtime/bake/mod.rs @@ -200,12 +200,11 @@ impl Framework { ) } - /// Sets up a per-graph - /// `Transpiler` in place. The full body lives in - /// `bake_body::Framework::init_transpiler_with_options`; this keystone - /// version operates on the keystone `BuildConfigSubset` (which omits - /// `conditions`/`env`/`define`/`drop` until the schema types are - /// const-constructible — those paths default). + /// Sets up a per-graph `Transpiler` in place via `init_transpiler_impl`, + /// with the DevServer defaults: source maps follow `mode` and the three + /// minify overrides always default to `mode != Development` regardless of + /// `BuildConfigSubset`. User-supplied source-map/minify flags are only + /// honored by `init_transpiler_with_options` (bake_body). /// Returns the arena slot for the `bake_types::Framework` projection; caller must `drop_in_place` it. pub fn init_transpiler<'a>( &mut self, @@ -216,79 +215,7 @@ impl Framework { out: &mut core::mem::MaybeUninit>, bundler_options: &BuildConfigSubset, ) -> Result<*mut bun_bundler::bake_types::Framework, bun_core::Error> { - use bun_options_types::schema as bun_schema; - - let mut ast_memory_allocator = bun_ast::ASTMemoryAllocator::borrowing(arena); - let _ast_scope = ast_memory_allocator.enter(); - - let out: &mut bun_bundler::Transpiler = out.write(bun_bundler::Transpiler::init( - arena, - log, - bun_schema::api::TransformOptions::default(), - None, - )?); - - out.options.target = match renderer { - Graph::Client => bun_ast::Target::Browser, - Graph::Server | Graph::Ssr => bun_ast::Target::Bun, - }; - out.options.public_path = match renderer { - Graph::Client => dev_server::CLIENT_PREFIX.as_bytes().into(), - Graph::Server | Graph::Ssr => Box::default(), - }; - out.options.entry_points = Box::default(); - out.options.log = log; - out.options.output_format = match mode { - Mode::Development => bun_bundler::options::Format::InternalBakeDev, - Mode::ProductionDynamic | Mode::ProductionStatic => bun_bundler::options::Format::Esm, - }; - out.options.out_extensions = bun_collections::StringHashMap::new(); - out.options.hot_module_reloading = mode == Mode::Development; - out.options.code_splitting = mode != Mode::Development; - out.options.output_dir = Box::default(); - - out.options.react_fast_refresh = mode == Mode::Development - && renderer == Graph::Client - && self.react_fast_refresh.is_some(); - out.options.server_components = self.server_components.is_some(); - - out.options.conditions = bun_bundler::options::ESMConditions::init( - out.options.target.default_conditions(), - out.options.target.is_server_side(), - bundler_options.conditions.keys(), - )?; - if renderer == Graph::Server && self.server_components.is_some() { - out.options.conditions.append_slice(&[b"react-server"])?; - } - if mode == Mode::Development { - out.options.conditions.append_slice(&[b"development"])?; - } - if matches!(renderer, Graph::Server | Graph::Ssr) { - out.options.conditions.append_slice(&[b"node"])?; - } - - out.options.production = mode != Mode::Development; - out.options.tree_shaking = mode != Mode::Development; - // The three minify overrides always default to `mode != Development` - // here regardless of `BuildConfigSubset`. User-supplied minify flags - // are only honored by `init_transpiler_with_options` (bake_body). - out.options.minify_syntax = mode != Mode::Development; - out.options.minify_identifiers = mode != Mode::Development; - out.options.minify_whitespace = mode != Mode::Development; - out.options.css_chunking = true; - // The bundler crate (lower tier) carries a TYPE_ONLY - // projection (`bake_types::Framework`); construct it here and give it - // arena lifetime so `BundleOptions<'a>` can borrow it for the bundle pass. - let framework_view: *mut bun_bundler::bake_types::Framework = - arena.alloc(self.as_bundler_view()); - // SAFETY: `arena.alloc` returns a non-null, initialized pointer backed by `arena: &'a Arena`, - // which outlives `out: &mut Transpiler<'a>`, so borrowing it as `&'a Framework` is sound. - out.options.framework = Some(unsafe { &*framework_view }); - out.options.inline_entrypoint_import_meta_main = true; - if let Some(ignore) = bundler_options.ignore_dce_annotations { - out.options.ignore_dce_annotations = ignore; - } - out.options.source_map = match mode { + let source_map = match mode { // Source maps must always be external, as DevServer special cases // the linking and part of the generation of these. It also relies // on source maps always being enabled. @@ -298,65 +225,23 @@ impl Framework { bun_bundler::options::SourceMapOption::None } }; - if bundler_options.env != bun_schema::api::DotEnvBehavior::_none { - out.options.env.behavior = bundler_options.env; - out.options.env.prefix = bundler_options.env_prefix.unwrap_or(b"").into(); - } - // The resolver crate carries a FORWARD_DECL subset of `BundleOptions`, so - // re-project via the dedicated helper rather than `Clone`. - out.sync_resolver_opts(); - - out.configure_linker(); - out.configure_defines()?; - out.options.jsx.development = mode == Mode::Development; - - bake_body::add_import_meta_defines( - &mut out.options.define, + init_transpiler_impl( + arena, + log, mode, - match renderer { - Graph::Client => Side::Client, - Graph::Server | Graph::Ssr => Side::Server, + renderer, + out, + bundler_options, + InitTranspilerOptions { + source_map, + minify_whitespace: None, + minify_syntax: None, + minify_identifiers: None, + has_react_fast_refresh: self.react_fast_refresh.is_some(), + has_server_components: self.server_components.is_some(), + framework_view: self.as_bundler_view(), }, - )?; - - if (bundler_options.define.keys.len() + bundler_options.drop.count()) > 0 { - debug_assert_eq!( - bundler_options.define.keys.len(), - bundler_options.define.values.len() - ); - use bun_bundler::DefineDataExt; - for (k, v) in bundler_options - .define - .keys - .iter() - .zip(bundler_options.define.values.iter()) - { - let parsed = - bun_bundler::defines::DefineData::parse(k, v, false, false, log, arena)?; - out.options.define.insert(k, parsed)?; - } - - for drop_item in bundler_options.drop.keys() { - if !drop_item.is_empty() { - let parsed = bun_bundler::defines::DefineData::parse( - drop_item, b"", true, true, log, arena, - )?; - out.options.define.insert(drop_item, parsed)?; - } - } - } - - if mode != Mode::Development { - // Hide information about the source repository, at the cost of debugging quality. - out.options.entry_naming = b"_bun/[hash].[ext]".as_slice().into(); - out.options.chunk_naming = b"_bun/[hash].[ext]".as_slice().into(); - out.options.asset_naming = b"_bun/[hash].[ext]".as_slice().into(); - } - - // Re-sync after define/naming mutations so the - // resolver sees the final option set. - out.sync_resolver_opts(); - Ok(framework_view) + ) } /// Resolves built-in module @@ -475,6 +360,181 @@ impl Framework { } } +/// Caller-specific inputs to `init_transpiler_impl`: the two `Framework` +/// representations contribute their feature flags and `bake_types::Framework` +/// projection here. A `None` minify override defaults to +/// `mode != Development`. +pub(crate) struct InitTranspilerOptions { + pub source_map: bun_bundler::options::SourceMapOption, + pub minify_whitespace: Option, + pub minify_syntax: Option, + pub minify_identifiers: Option, + pub has_react_fast_refresh: bool, + pub has_server_components: bool, + pub framework_view: bun_bundler::bake_types::Framework, +} + +/// Shared body of `Framework::init_transpiler` (keystone, DevServer) and +/// `bake_body::Framework::init_transpiler_with_options` (production): wires +/// the per-graph transpiler options (target/conditions/minify/source +/// map/define/drop) that are identical between the two `Framework` +/// representations, which only contribute the `InitTranspilerOptions` here. +/// +/// Returns the arena slot for the projection; the caller must `drop_in_place` +/// it — interior `Box<[u8]>` are not dropped by bumpalo. (The production path +/// deliberately leaks it: bounded per-session, revisit when +/// `bake_types::BuiltInModule` is reshaped to `&'a [u8]`.) +pub(crate) fn init_transpiler_impl<'a>( + arena: &'a bun_alloc::Arena, + log: &mut bun_ast::Log, + mode: Mode, + renderer: Graph, + out: &mut core::mem::MaybeUninit>, + bundler_options: &BuildConfigSubset, + opts: InitTranspilerOptions, +) -> Result<*mut bun_bundler::bake_types::Framework, bun_core::Error> { + use bun_options_types::schema as bun_schema; + + // `ASTMemoryAllocator::enter` returns an RAII `Scope` whose `Drop` runs + // `exit()` at end-of-fn. + let mut ast_memory_allocator = bun_ast::ASTMemoryAllocator::borrowing(arena); + let _ast_scope = ast_memory_allocator.enter(); + + // The caller hands us an uninitialized slot, so use `MaybeUninit::write` + // (no drop of prior bytes) then reborrow as `&mut Transpiler` for the + // field assignments below. + let out: &mut bun_bundler::Transpiler = out.write(bun_bundler::Transpiler::init( + arena, + log, + bun_schema::api::TransformOptions::default(), + None, + )?); + + out.options.target = match renderer { + Graph::Client => bun_ast::Target::Browser, + Graph::Server | Graph::Ssr => bun_ast::Target::Bun, + }; + out.options.public_path = match renderer { + Graph::Client => dev_server::CLIENT_PREFIX.as_bytes().into(), + Graph::Server | Graph::Ssr => Box::default(), + }; + out.options.entry_points = Box::default(); + out.options.log = log; + out.options.output_format = match mode { + Mode::Development => bun_bundler::options::Format::InternalBakeDev, + Mode::ProductionDynamic | Mode::ProductionStatic => bun_bundler::options::Format::Esm, + }; + out.options.out_extensions = bun_collections::StringHashMap::new(); + out.options.hot_module_reloading = mode == Mode::Development; + out.options.code_splitting = mode != Mode::Development; + + // force disable filesystem output, even though bundle_v2 + // is special cased to return before that code is reached. + out.options.output_dir = Box::default(); + + // framework configuration + out.options.react_fast_refresh = + mode == Mode::Development && renderer == Graph::Client && opts.has_react_fast_refresh; + out.options.server_components = opts.has_server_components; + + out.options.conditions = bun_bundler::options::ESMConditions::init( + out.options.target.default_conditions(), + out.options.target.is_server_side(), + bundler_options.conditions.keys(), + )?; + if renderer == Graph::Server && opts.has_server_components { + out.options.conditions.append_slice(&[b"react-server"])?; + } + if mode == Mode::Development { + // Support `esm-env` package using this condition. + out.options.conditions.append_slice(&[b"development"])?; + } + // Ensure "node" condition is included for server-side rendering + // This helps with package.json imports field resolution + if matches!(renderer, Graph::Server | Graph::Ssr) { + out.options.conditions.append_slice(&[b"node"])?; + } + + out.options.production = mode != Mode::Development; + out.options.tree_shaking = mode != Mode::Development; + out.options.minify_syntax = opts.minify_syntax.unwrap_or(mode != Mode::Development); + out.options.minify_identifiers = opts.minify_identifiers.unwrap_or(mode != Mode::Development); + out.options.minify_whitespace = opts.minify_whitespace.unwrap_or(mode != Mode::Development); + out.options.css_chunking = true; + // The bundler crate (lower tier) carries a TYPE_ONLY projection + // (`bake_types::Framework`); arena-allocate it here so `BundleOptions<'a>` + // can borrow it for the bundle pass. + let framework_view: *mut bun_bundler::bake_types::Framework = arena.alloc(opts.framework_view); + // SAFETY: `arena.alloc` returns a non-null, initialized pointer backed by `arena: &'a Arena`, + // which outlives `out: &mut Transpiler<'a>`, so borrowing it as `&'a Framework` is sound. + out.options.framework = Some(unsafe { &*framework_view }); + out.options.inline_entrypoint_import_meta_main = true; + if let Some(ignore) = bundler_options.ignore_dce_annotations { + out.options.ignore_dce_annotations = ignore; + } + + out.options.source_map = opts.source_map; + if bundler_options.env != bun_schema::api::DotEnvBehavior::_none { + out.options.env.behavior = bundler_options.env; + out.options.env.prefix = bundler_options.env_prefix.unwrap_or(b"").into(); + } + // The resolver crate carries a FORWARD_DECL subset of `BundleOptions`, so + // re-project via the dedicated helper rather than `Clone`. + out.sync_resolver_opts(); + + out.configure_linker(); + out.configure_defines()?; + + out.options.jsx.development = mode == Mode::Development; + + bake_body::add_import_meta_defines( + &mut out.options.define, + mode, + match renderer { + Graph::Client => Side::Client, + Graph::Server | Graph::Ssr => Side::Server, + }, + )?; + + if (bundler_options.define.keys.len() + bundler_options.drop.count()) > 0 { + debug_assert_eq!( + bundler_options.define.keys.len(), + bundler_options.define.values.len() + ); + use bun_bundler::DefineDataExt; + for (k, v) in bundler_options + .define + .keys + .iter() + .zip(bundler_options.define.values.iter()) + { + let parsed = bun_bundler::defines::DefineData::parse(k, v, false, false, log, arena)?; + out.options.define.insert(k, parsed)?; + } + + for drop_item in bundler_options.drop.keys() { + if !drop_item.is_empty() { + let parsed = bun_bundler::defines::DefineData::parse( + drop_item, b"", true, true, log, arena, + )?; + out.options.define.insert(drop_item, parsed)?; + } + } + } + + if mode != Mode::Development { + // Hide information about the source repository, at the cost of debugging quality. + out.options.entry_naming = b"_bun/[hash].[ext]".as_slice().into(); + out.options.chunk_naming = b"_bun/[hash].[ext]".as_slice().into(); + out.options.asset_naming = b"_bun/[hash].[ext]".as_slice().into(); + } + + // Re-sync after define/naming mutations so the resolver sees the + // final option set. + out.sync_resolver_opts(); + Ok(framework_view) +} + /// `bake.SplitBundlerOptions` — per-graph bundler config + shared plugin. #[derive(Default)] pub struct SplitBundlerOptions { @@ -555,56 +615,24 @@ impl From for Framework { } } } -impl From for BuildConfigSubset { - fn from(src: bake_body::BuildConfigSubset) -> Self { - // `BuildConfigSubset` mirrors the field-set - // `Framework::init_transpiler` reads (everything except `loader` / - // `source_map`, which only `init_transpiler_with_options` honours). - Self { - ignore_dce_annotations: src.ignore_dce_annotations, - conditions: src.conditions, - drop: src.drop, - env: src.env, - env_prefix: src.env_prefix, - define: src.define, - minify_syntax: src.minify_syntax, - minify_identifiers: src.minify_identifiers, - minify_whitespace: src.minify_whitespace, - } - } -} impl From for SplitBundlerOptions { fn from(src: bake_body::SplitBundlerOptions) -> Self { Self { // `bake_body::Plugin` and keystone `jsc::Plugin` both alias // `crate::api::js_bundler::Plugin` — same nominal type, no cast. plugin: src.plugin, - client: src.client.into(), - server: src.server.into(), - ssr: src.ssr.into(), + client: src.client, + server: src.server, + ssr: src.ssr, } } } /// `bake.SplitBundlerOptions.BuildConfigSubset`. Full body (with `from_js`) -/// lives in `bake_body.rs`; this keystone mirror carries every field that -/// `Framework::init_transpiler` reads so DevServer's -/// per-graph transpilers see bunfig `[serve.static]` define/env/conditions. -#[derive(Default)] -pub struct BuildConfigSubset { - pub ignore_dce_annotations: Option, - pub conditions: bun_collections::ArrayHashMap<&'static [u8], ()>, - pub drop: bun_collections::ArrayHashMap<&'static [u8], ()>, - pub env: bun_options_types::schema::api::DotEnvBehavior, - pub env_prefix: Option<&'static [u8]>, - pub define: bun_options_types::schema::api::StringMap, - pub minify_syntax: Option, - pub minify_identifiers: Option, - pub minify_whitespace: Option, - // `loader`/`source_map` intentionally omitted — only - // `init_transpiler_with_options` (bake_body) honours those, and DevServer - // never calls that path. -} +/// lives in `bake_body.rs`; DevServer's `init_transpiler` reads everything +/// except `loader`/`source_map`, which only `init_transpiler_with_options` +/// honours. +pub use bake_body::BuildConfigSubset; /// `bake.HmrRuntime` — embedded HMR runtime code + precomputed line count. /// Canonical definition; `bake_body::HmrRuntime` re-exports this diff --git a/src/runtime/bake/production.rs b/src/runtime/bake/production.rs index f61de35ad562..b14b9bde63e8 100644 --- a/src/runtime/bake/production.rs +++ b/src/runtime/bake/production.rs @@ -35,7 +35,6 @@ use bun_resolver as resolver; use crate::cli::command::{Context, HotReload}; use bun_options_types::context::MacroOptions; -use bun_options_types::offline_mode::OfflineMode; use bun_bundler::options::OutputKind; @@ -135,35 +134,7 @@ pub fn build_command(ctx: Context) -> Result<(), bun_core::Error> { vm.argv.clone_from(&ctx.passthrough); vm.arena = NonNull::new(&raw mut arena); // vm.allocator = arena.arena() — dropped per §Allocators - // `BundleOptions.install` is `Option>`, so no - // lifetime-extension cast is needed. - let install_ptr = ctx.install.as_deref().map(NonNull::from); - b.options.install = install_ptr; - b.resolver.opts.install = install_ptr; - b.resolver.opts.global_cache = ctx.debug.global_cache; - b.resolver.opts.prefer_offline_install = ctx - .debug - .offline_mode_setting - .unwrap_or(OfflineMode::Online) - == OfflineMode::Offline; - // Note: `bun_resolver::options::BundleOptions` has no - // `prefer_latest_install` field; compute the value once - // and assign only to `b.options` (which does carry it). The resolver - // never reads it. - let prefer_latest = ctx - .debug - .offline_mode_setting - .unwrap_or(OfflineMode::Online) - == OfflineMode::Latest; - b.options.global_cache = b.resolver.opts.global_cache; - b.options.prefer_offline_install = b.resolver.opts.prefer_offline_install; - b.options.prefer_latest_install = prefer_latest; - // SAFETY: `b.env` is the Transpiler-owned `*mut Loader`; store it - // as `NonNull` (not `&Loader`) because `configure_defines()` below - // reborrows the same allocation as `&mut Loader` via `run_env_loader()`, - // which would alias a live `&Loader` here. The Loader outlives the - // resolver (process-lifetime singleton or VM-owned). - b.resolver.env_loader = NonNull::new(b.env); + crate::cli::run_command::wire_install_options(b, ctx); b.options.minify_identifiers = ctx.bundler_options.minify_identifiers; b.options.minify_whitespace = ctx.bundler_options.minify_whitespace; b.options.ignore_dce_annotations = ctx.bundler_options.ignore_dce_annotations; diff --git a/src/runtime/cli/bunx_command.rs b/src/runtime/cli/bunx_command.rs index 92c834d0ec89..4319db583f39 100644 --- a/src/runtime/cli/bunx_command.rs +++ b/src/runtime/cli/bunx_command.rs @@ -216,6 +216,19 @@ pub(crate) enum GetBinNameError { bun_core::named_error_set!(GetBinNameError); +/// 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. @@ -592,6 +605,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]>], + ) -> Result<(), bun_core::Error> { + 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); @@ -1380,61 +1464,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 @@ -1446,55 +1492,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 fd56b59de8b4..879ef32bfb68 100644 --- a/src/runtime/cli/create_command.rs +++ b/src/runtime/cli/create_command.rs @@ -1687,64 +1687,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 97e554a7ae93..588eb2afa3f7 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(()) } @@ -591,87 +585,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 cfg!(debug_assertions) { - 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)), + ) } } @@ -944,18 +862,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 8a3fdc9b5c36..6cdb1613fed0 100644 --- a/src/runtime/cli/install_command.rs +++ b/src/runtime/cli/install_command.rs @@ -1,4 +1,3 @@ -use bun_bundler::bundle_v2::{DependenciesScanner, DependenciesScannerResult}; use bun_core::{Error, Global, Output, err}; use bun_install::package_manager_real::{ CommandLineArguments, PackageManager, ROOT_PACKAGE_JSON_PATH, Subcommand, install_with_manager, @@ -6,7 +5,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; @@ -46,97 +45,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<(), 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())?; - - 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 51ad95c8e69d..e8c9b09f618d 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, +) -> Result<(Lockfile, Package), bun_core::Error> { + 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, +) -> 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 {}", + e.name(), + ); + } + Global::crash(); + } + } +} + fn link(ctx: command::Context) -> Result<(), bun_core::Error> { 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) -> Result<(), bun_core::Error> { 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 {}", - 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 69ad7c52aa8a..9e9a3f965345 100644 --- a/src/runtime/cli/mod.rs +++ b/src/runtime/cli/mod.rs @@ -390,6 +390,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 f1713554a0f8..5f0527376422 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 bun_collections::{StringArrayHashMap, VecExt}; @@ -13,6 +13,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; @@ -21,8 +24,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; @@ -247,16 +249,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(()) } @@ -509,84 +502,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 cfg!(debug_assertions) { - 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)), + ) } } @@ -1139,18 +1059,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 272b53c6aeec..e3f88f73fe29 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, ) -> Result<(), bun_core::Error> { - // 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 { ) -> Result<(), bun_core::Error> { 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 51244523ce8d..9ca576cf0b4e 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 @@ -1856,7 +1854,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 { @@ -1961,7 +1947,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") { @@ -2191,33 +2202,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 { @@ -2611,11 +2596,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, @@ -2632,142 +2615,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() { @@ -3221,6 +3103,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 a14d7e37f4a3..55dc946516dc 100644 --- a/src/runtime/cli/pm_update_package_json.rs +++ b/src/runtime/cli/pm_update_package_json.rs @@ -16,7 +16,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, @@ -42,95 +42,113 @@ 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<(), 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) + }); + } + + update_package_json_and_install_and_cli(ctx, subcommand, cli) +} + +/// 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<(), 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"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())?; + (this.install)(ctx, cli.clone())?; - 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) + // `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 9e61285f8ef9..e5dd53d7a67a 100644 --- a/src/runtime/cli/repl.rs +++ b/src/runtime/cli/repl.rs @@ -628,6 +628,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", @@ -1303,19 +1313,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 @@ -1337,7 +1348,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 @@ -1360,7 +1371,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. @@ -1373,18 +1384,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); - 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); + 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(); @@ -1404,7 +1419,7 @@ impl<'a> Repl<'a> { self.set_last_error(exc); self.print_js_error(exc); vm_mut(vm).tick(); - return; + return None; } }; if let Some(value) = maybe_value { @@ -1412,7 +1427,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) @@ -1422,6 +1437,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)); @@ -1433,7 +1456,9 @@ impl<'a> Repl<'a> { } // Tick the event loop to handle any pending work - vm_mut(vm).tick(); + if let Some(vm) = self.vm { + vm_mut(vm).tick(); + } } /// Evaluate a script from `bun repl -e/--eval` or `-p/--print` non-interactively. @@ -1612,103 +1637,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_mut(vm).wait_for_promise(jsc::AnyPromise::Normal(promise)); - if vm.jsc_vm().execution_forbidden() { - vm_set_execution_forbidden(vm.jsc_vm, 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_mut(vm).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); - 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_mut(vm).tick(); } - vm_mut(vm).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 9cc0e0f19db0..357ef3e2dd7e 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 a98d7bce7f27..6da406d82fd3 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 { @@ -781,27 +811,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..e52e9ed35043 --- /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 cfg!(debug_assertions) { + 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 9b3cd52a0f9b..35a56865fa9a 100644 --- a/src/runtime/cli/unlink_command.rs +++ b/src/runtime/cli/unlink_command.rs @@ -5,13 +5,11 @@ use bun_core::{Global, Output, err}; 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) -> Result<(), bun_core::Error> { 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) -> Result<(), bun_core::Error> { } // 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 {}", - 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 9250567dacae..54566caef272 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, ) -> Result<(), bun_core::Error> { - // 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, ) -> Result, bun_core::Error> { 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 abaaa7a42b39..e591693964af 100644 --- a/src/runtime/crypto/CryptoHasher.rs +++ b/src/runtime/crypto/CryptoHasher.rs @@ -49,6 +49,97 @@ fn is_bun_file_blob(input: &BlobOrStringOrBuffer) -> bool { } } +/// Parsed form of the optional `digest()`/`hash()` output argument: either a +/// caller-provided byte sink (`None` → allocate a fresh buffer), or an +/// encoding name to stringify the digest with. +enum DigestOutput { + Bytes(Option), + Encoding(Encoding), +} + +fn parse_digest_output( + global: &JSGlobalObject, + output: Option, +) -> JsResult { + let Some(string_or_buffer) = output else { + return Ok(DigestOutput::Bytes(None)); + }; + if let StringOrBuffer::Buffer(buffer) = &string_or_buffer { + return Ok(DigestOutput::Bytes(Some(buffer.buffer))); + } + // `inline else => |*str|` — every non-buffer arm yields a string-like + // `defer str.deinit()` — handled by Drop. + let Some(encoding) = Encoding::from(string_or_buffer.slice()) else { + return Err(global + .err( + ErrorCode::INVALID_ARG_VALUE, + format_args!( + "Unknown encoding: {}", + bstr::BStr::new(string_or_buffer.slice()) + ), + ) + .throw()); + }; + Ok(DigestOutput::Encoding(encoding)) +} + +/// Hand-expanded `wrapInstanceMethod` decode for the trailing +/// `?Node.StringOrBuffer` parameter (instance-method arm: +/// empty/undefined/null → None). +fn digest_output_argument( + global: &JSGlobalObject, + callframe: &CallFrame, +) -> JsResult> { + let arguments = callframe.arguments_old::<1>(); + if arguments.len > 0 { + let arg = arguments.ptr[0]; + if !arg.is_empty_or_undefined_or_null() { + return match StringOrBuffer::from_js(global, arg)? { + Some(v) => Ok(Some(v)), + None => { + Err(global.throw_invalid_arguments(format_args!("expected string or buffer"))) + } + }; + } + } + Ok(None) +} + +/// Hand-expanded static-method decode for the `Node.BlobOrStringOrBuffer` +/// input parameter. +fn hash_input_argument( + global: &JSGlobalObject, + arg: Option, +) -> JsResult { + if let Some(arg) = arg { + if let Some(b) = BlobOrStringOrBuffer::from_js(global, arg)? { + return Ok(b); + } + } + Err(global.throw_invalid_arguments(format_args!("expected blob, string or buffer"))) +} + +/// Hand-expanded static-method decode for the trailing `?Node.StringOrBuffer` +/// output parameter (static-method arm: only `undefined` → None). +fn hash_output_argument( + global: &JSGlobalObject, + arg: Option, +) -> JsResult> { + match arg { + Some(arg) => match StringOrBuffer::from_js(global, arg)? { + Some(v) => Ok(Some(v)), + None => { + if arg.is_undefined() { + Ok(None) + } else { + Err(global.throw_invalid_arguments(format_args!("expected string or buffer"))) + } + } + }, + None => Ok(None), + } +} + /// `union(enum)` → Rust enum with payload variants. /// `.classes.ts`-backed type: the C++ JSCell wrapper stays generated; this is the `m_ctx` payload. /// @@ -218,24 +309,7 @@ impl CryptoHasher { global: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let arguments = callframe.arguments_old::<1>(); - // ?Node.StringOrBuffer (instance-method arm: empty/undefined/null → None) - let output: Option = if arguments.len > 0 { - let arg = arguments.ptr[0]; - if !arg.is_empty_or_undefined_or_null() { - match StringOrBuffer::from_js(global, arg)? { - Some(v) => Some(v), - None => { - return Err(global - .throw_invalid_arguments(format_args!("expected string or buffer"))); - } - } - } else { - None - } - } else { - None - }; + let output = digest_output_argument(global, callframe)?; Self::digest_(this, global, output) } @@ -243,58 +317,20 @@ impl CryptoHasher { /// `(algorithm string, input, optional output buffer/encoding)`. pub fn hash(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { let arguments = callframe.arguments_old::<3>(); - let mut i = 0usize; - let mut next_eat = || { - if i < arguments.len { - let v = arguments.ptr[i]; - i += 1; - Some(v) - } else { - None - } - }; let algorithm = { - let Some(string_value) = next_eat() else { + if arguments.len == 0 { return Err(global.throw_invalid_arguments(format_args!("Missing argument"))); - }; + } + let string_value = arguments.ptr[0]; if string_value.is_undefined_or_null() { return Err(global.throw_invalid_arguments(format_args!("Expected string"))); } string_value.get_zig_string(global)? }; - // Node.BlobOrStringOrBuffer - let input = { - let Some(arg) = next_eat() else { - return Err( - global.throw_invalid_arguments(format_args!("expected blob, string or buffer")) - ); - }; - match BlobOrStringOrBuffer::from_js(global, arg)? { - Some(b) => b, - None => { - return Err(global - .throw_invalid_arguments(format_args!("expected blob, string or buffer"))); - } - } - }; - - // ?Node.StringOrBuffer (static-method arm: only `undefined` → None) - let output: Option = match next_eat() { - Some(arg) => match StringOrBuffer::from_js(global, arg)? { - Some(v) => Some(v), - None => { - if arg.is_undefined() { - None - } else { - return Err(global - .throw_invalid_arguments(format_args!("expected string or buffer"))); - } - } - }, - None => None, - }; + let input = hash_input_argument(global, (arguments.len > 1).then(|| arguments.ptr[1]))?; + let output = hash_output_argument(global, (arguments.len > 2).then(|| arguments.ptr[2]))?; Self::hash_(global, algorithm, &input, output) } @@ -437,28 +473,11 @@ impl CryptoHasher { }; // `defer evp.deinit()` — handled by Drop on `evp`. - if let Some(string_or_buffer) = output { - if let StringOrBuffer::Buffer(buffer) = &string_or_buffer { - let ab = buffer.buffer; - return Self::hash_to_bytes(global, &mut evp, input, Some(ab)); + match parse_digest_output(global, output)? { + DigestOutput::Bytes(ab) => Self::hash_to_bytes(global, &mut evp, input, ab), + DigestOutput::Encoding(encoding) => { + Self::hash_to_encoding(global, &mut evp, input, encoding) } - // `inline else => |*str|` — every non-buffer arm yields a string-like - // `defer str.deinit()` — handled by Drop. - let Some(encoding) = Encoding::from(string_or_buffer.slice()) else { - return Err(global - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!( - "Unknown encoding: {}", - bstr::BStr::new(string_or_buffer.slice()) - ), - ) - .throw()); - }; - - Self::hash_to_encoding(global, &mut evp, input, encoding) - } else { - Self::hash_to_bytes(global, &mut evp, input, None) } } @@ -670,27 +689,9 @@ impl CryptoHasher { global: &JSGlobalObject, output: Option, ) -> JsResult { - if let Some(string_or_buffer) = output { - if let StringOrBuffer::Buffer(buffer) = &string_or_buffer { - let ab = buffer.buffer; - return this.digest_to_bytes(global, Some(ab)); - } - // `defer str.deinit()` — handled by Drop. - let Some(encoding) = Encoding::from(string_or_buffer.slice()) else { - return Err(global - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!( - "Unknown encoding: {}", - bstr::BStr::new(string_or_buffer.slice()) - ), - ) - .throw()); - }; - - this.digest_to_encoding(global, encoding) - } else { - this.digest_to_bytes(global, None) + match parse_digest_output(global, output)? { + DigestOutput::Bytes(ab) => this.digest_to_bytes(global, ab), + DigestOutput::Encoding(encoding) => this.digest_to_encoding(global, encoding), } } @@ -909,30 +910,15 @@ impl CryptoHasherZig { input: &BlobOrStringOrBuffer, output: Option, ) -> JsResult { - if let Some(string_or_buffer) = output { - if let StringOrBuffer::Buffer(buffer) = &string_or_buffer { - let ab = buffer.buffer; - return Self::hash_by_name_inner_to_bytes::(global, input, Some(ab)); + match parse_digest_output(global, output)? { + DigestOutput::Bytes(ab) => Self::hash_by_name_inner_to_bytes::(global, input, ab), + DigestOutput::Encoding(Encoding::Buffer) => { + Self::hash_by_name_inner_to_bytes::(global, input, None) } - let Some(encoding) = Encoding::from(string_or_buffer.slice()) else { - return Err(global - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!( - "Unknown encoding: {}", - bstr::BStr::new(string_or_buffer.slice()) - ), - ) - .throw()); - }; - - if encoding == Encoding::Buffer { - return Self::hash_by_name_inner_to_bytes::(global, input, None); + DigestOutput::Encoding(encoding) => { + Self::hash_by_name_inner_to_string::(global, input, encoding) } - - return Self::hash_by_name_inner_to_string::(global, input, encoding); } - Self::hash_by_name_inner_to_bytes::(global, input, None) } fn hash_by_name_inner_to_string( @@ -1207,24 +1193,7 @@ impl StaticCryptoHasher { global: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let arguments = callframe.arguments_old::<1>(); - // ?Node.StringOrBuffer (instance-method arm: empty/undefined/null → None) - let output: Option = if arguments.len > 0 { - let arg = arguments.ptr[0]; - if !arg.is_empty_or_undefined_or_null() { - match StringOrBuffer::from_js(global, arg)? { - Some(v) => Some(v), - None => { - return Err(global - .throw_invalid_arguments(format_args!("expected string or buffer"))); - } - } - } else { - None - } - } else { - None - }; + let output = digest_output_argument(global, callframe)?; Self::digest_(this, global, output) } @@ -1234,49 +1203,8 @@ impl StaticCryptoHasher { /// `(*JSGlobalObject, Node.BlobOrStringOrBuffer, ?Node.StringOrBuffer)`. pub fn hash(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { let arguments = callframe.arguments_old::<2>(); - let mut i = 0usize; - let mut next_eat = || { - if i < arguments.len { - let v = arguments.ptr[i]; - i += 1; - Some(v) - } else { - None - } - }; - - // Node.BlobOrStringOrBuffer - let input = { - let Some(arg) = next_eat() else { - return Err( - global.throw_invalid_arguments(format_args!("expected blob, string or buffer")) - ); - }; - match BlobOrStringOrBuffer::from_js(global, arg)? { - Some(b) => b, - None => { - return Err(global - .throw_invalid_arguments(format_args!("expected blob, string or buffer"))); - } - } - }; - - // ?Node.StringOrBuffer (static-method arm: only `undefined` → None) - let output: Option = match next_eat() { - Some(arg) => match StringOrBuffer::from_js(global, arg)? { - Some(v) => Some(v), - None => { - if arg.is_undefined() { - None - } else { - return Err(global - .throw_invalid_arguments(format_args!("expected string or buffer"))); - } - } - }, - None => None, - }; - + let input = hash_input_argument(global, (arguments.len > 0).then(|| arguments.ptr[0]))?; + let output = hash_output_argument(global, (arguments.len > 1).then(|| arguments.ptr[1]))?; Self::hash_(global, &input, output) } @@ -1314,29 +1242,37 @@ impl StaticCryptoHasher { encoding.encode_with_max_size(global, EVP_MAX_MD_SIZE_USIZE, output_digest_buf.as_ref()) } + /// Validate the optional caller-provided output buffer and return the + /// destination digest array (falling back to `fallback`). + fn output_digest<'a>( + global: &JSGlobalObject, + output: Option<&ArrayBuffer>, + fallback: &'a mut H::Digest, + ) -> JsResult<&'a mut H::Digest> { + let Some(output_buf) = output else { + return Ok(fallback); + }; + if output_buf.byte_slice().len() < H::DIGEST { + return Err(global.throw_invalid_arguments(format_args!( + "TypedArray must be at least {} bytes", + H::DIGEST + ))); + } + // SAFETY: `byte_slice().len() >= H::DIGEST` checked above; + // `H::Digest = [u8; H::DIGEST]`; `output_buf.ptr` is the JSC-owned + // writable backing store. Build the `&mut` directly from the raw + // `*mut u8` field — never via `&[u8].as_ptr()` (Stacked-Borrows UB). + Ok(unsafe { &mut *output_buf.ptr.cast::() }) + } + fn hash_to_bytes( global: &JSGlobalObject, input: &BlobOrStringOrBuffer, output: Option, ) -> JsResult { let mut output_digest_buf: H::Digest = H::new_digest(); - let output_digest_slice: &mut H::Digest; - if let Some(output_buf) = &output { - let bytes_len = output_buf.byte_slice().len(); - if bytes_len < H::DIGEST { - return Err(global.throw_invalid_arguments(format_args!( - "TypedArray must be at least {} bytes", - H::DIGEST - ))); - } - // SAFETY: `bytes_len >= H::DIGEST` checked above; `H::Digest = [u8; H::DIGEST]`; - // `output_buf.ptr` is the JSC-owned writable backing store. Build the - // `&mut` directly from the raw `*mut u8` field — never via - // `&[u8].as_ptr()` (Stacked-Borrows UB). - output_digest_slice = unsafe { &mut *output_buf.ptr.cast::() }; - } else { - output_digest_slice = &mut output_digest_buf; - } + let output_digest_slice = + Self::output_digest(global, output.as_ref(), &mut output_digest_buf)?; // SAFETY: `boring_engine` returns the VM-owned engine (live for the // process) or null; the else arm passes null. @@ -1368,26 +1304,9 @@ impl StaticCryptoHasher { ))); } - if let Some(string_or_buffer) = output { - if let StringOrBuffer::Buffer(buffer) = &string_or_buffer { - let ab = buffer.buffer; - return Self::hash_to_bytes(global, input, Some(ab)); - } - let Some(encoding) = Encoding::from(string_or_buffer.slice()) else { - return Err(global - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!( - "Unknown encoding: {}", - bstr::BStr::new(string_or_buffer.slice()) - ), - ) - .throw()); - }; - - Self::hash_to_encoding(global, input, encoding) - } else { - Self::hash_to_bytes(global, input, None) + match parse_digest_output(global, output)? { + DigestOutput::Bytes(ab) => Self::hash_to_bytes(global, input, ab), + DigestOutput::Encoding(encoding) => Self::hash_to_encoding(global, input, encoding), } } @@ -1458,26 +1377,9 @@ impl StaticCryptoHasher { ) .throw()); } - if let Some(string_or_buffer) = output { - if let StringOrBuffer::Buffer(buffer) = &string_or_buffer { - let ab = buffer.buffer; - return this.digest_to_bytes(global, Some(ab)); - } - let Some(encoding) = Encoding::from(string_or_buffer.slice()) else { - return Err(global - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!( - "Unknown encoding: {}", - bstr::BStr::new(string_or_buffer.slice()) - ), - ) - .throw()); - }; - - this.digest_to_encoding(global, encoding) - } else { - this.digest_to_bytes(global, None) + match parse_digest_output(global, output)? { + DigestOutput::Bytes(ab) => this.digest_to_bytes(global, ab), + DigestOutput::Encoding(encoding) => this.digest_to_encoding(global, encoding), } } @@ -1487,23 +1389,8 @@ impl StaticCryptoHasher { output: Option, ) -> JsResult { let mut output_digest_buf: H::Digest = H::new_digest(); - let output_digest_slice: &mut H::Digest; - if let Some(output_buf) = &output { - let bytes_len = output_buf.byte_slice().len(); - if bytes_len < H::DIGEST { - return Err(global.throw_invalid_arguments(format_args!( - "TypedArray must be at least {} bytes", - H::DIGEST - ))); - } - // SAFETY: `bytes_len >= H::DIGEST`; `H::Digest = [u8; H::DIGEST]`; - // `output_buf.ptr` is the JSC-owned writable backing store. Build the - // `&mut` directly from the raw `*mut u8` field — never via - // `&[u8].as_ptr()` (Stacked-Borrows UB). - output_digest_slice = unsafe { &mut *output_buf.ptr.cast::() }; - } else { - output_digest_slice = &mut output_digest_buf; - } + let output_digest_slice = + Self::output_digest(global, output.as_ref(), &mut output_digest_buf)?; self.hashing.with_mut(|h| h.final_(output_digest_slice)); self.digested.set(true); @@ -1511,7 +1398,7 @@ impl StaticCryptoHasher { if let Some(output_buf) = output { Ok(output_buf.value) } else { - ArrayBuffer::create_uint8_array(global, output_digest_buf.as_ref()) + ArrayBuffer::create_uint8_array(global, output_digest_slice.as_ref()) } } diff --git a/src/runtime/crypto/PasswordObject.rs b/src/runtime/crypto/PasswordObject.rs index 950cc59d114f..307ab1d8fb0e 100644 --- a/src/runtime/crypto/PasswordObject.rs +++ b/src/runtime/crypto/PasswordObject.rs @@ -819,6 +819,40 @@ pub(crate) fn js_password_object_hash_sync( // ─── verify host functions ──────────────────────────────────────────────── +/// Parse the optional third `verify(password, hash, algorithm)` argument. +fn parse_verify_algorithm( + global_object: &JSGlobalObject, + arguments: &[JSValue], +) -> JsResult> { + let Some(&arg) = arguments.get(2) else { + return Ok(None); + }; + + if arg.is_empty_or_undefined_or_null() { + return Ok(None); + } + + if !arg.is_string() { + return Err(global_object.throw_invalid_argument_type("verify", "algorithm", "string")); + } + + let algorithm_string = arg.get_zig_string(global_object)?; + + match algorithm_from_zig_string(&algorithm_string) { + Some(a) => Ok(Some(a)), + None => { + if !global_object.has_exception() { + return Err(global_object.throw_invalid_argument_type( + "verify", + "algorithm", + UNKNOWN_PASSWORD_ALGORITHM_MESSAGE, + )); + } + Err(JsError::Thrown) + } + } +} + // Once we have bindings generator, this should be replaced with a generated function #[bun_jsc::host_fn] pub(crate) fn js_password_object_verify( @@ -832,29 +866,7 @@ pub(crate) fn js_password_object_verify( return Err(global_object.throw_not_enough_arguments("verify", 2, 0)); } - let mut algorithm: Option = None; - - if arguments.len() > 2 && !arguments[2].is_empty_or_undefined_or_null() { - if !arguments[2].is_string() { - return Err(global_object.throw_invalid_argument_type("verify", "algorithm", "string")); - } - - let algorithm_string = arguments[2].get_zig_string(global_object)?; - - algorithm = match algorithm_from_zig_string(&algorithm_string) { - Some(a) => Some(a), - None => { - if !global_object.has_exception() { - return Err(global_object.throw_invalid_argument_type( - "verify", - "algorithm", - UNKNOWN_PASSWORD_ALGORITHM_MESSAGE, - )); - } - return Err(JsError::Thrown); - } - }; - } + let algorithm = parse_verify_algorithm(global_object, arguments)?; // TODO: this most likely should error like `verifySync` instead of stringifying. // @@ -913,29 +925,7 @@ pub(crate) fn js_password_object_verify_sync( return Err(global_object.throw_not_enough_arguments("verify", 2, 0)); } - let mut algorithm: Option = None; - - if arguments.len() > 2 && !arguments[2].is_empty_or_undefined_or_null() { - if !arguments[2].is_string() { - return Err(global_object.throw_invalid_argument_type("verify", "algorithm", "string")); - } - - let algorithm_string = arguments[2].get_zig_string(global_object)?; - - algorithm = match algorithm_from_zig_string(&algorithm_string) { - Some(a) => Some(a), - None => { - if !global_object.has_exception() { - return Err(global_object.throw_invalid_argument_type( - "verify", - "algorithm", - UNKNOWN_PASSWORD_ALGORITHM_MESSAGE, - )); - } - return Ok(JSValue::ZERO); - } - }; - } + let algorithm = parse_verify_algorithm(global_object, arguments)?; let Some(password) = StringOrBuffer::from_js(global_object, arguments[0])? else { return Err(global_object.throw_invalid_argument_type( diff --git a/src/runtime/dns_jsc/dns.rs b/src/runtime/dns_jsc/dns.rs index 145e2a5aaa71..ec8cf815d866 100644 --- a/src/runtime/dns_jsc/dns.rs +++ b/src/runtime/dns_jsc/dns.rs @@ -194,7 +194,7 @@ pub(super) mod lib_info { return lib_c::lookup(this, query, global_this); }; - let key = get_addr_info_request::PendingCacheKey::init(query); + let key = PendingCacheKey::init_query(query); let cache = this.get_or_put_into_pending_cache(&key, PendingCacheField::PendingHostCacheNative); @@ -253,7 +253,7 @@ pub(super) mod lib_info { // SAFETY: request is exclusively owned; freed below via heap::take. unsafe { if (*request).cache.pending_cache() { - // Release the pending-cache slot. `getOrPutIntoPendingCache` already + // Release the pending-cache slot. `get_or_put_into_pending_cache` already // set the `used` bit, so failing to unset it here permanently orphans // the slot and leaves `buffer[pos].lookup` pointing at the request we // are about to free (UAF on the next `.inflight` hit). @@ -322,7 +322,7 @@ pub(super) mod lib_c { query_init: &GetAddrInfo, global_this: &JSGlobalObject, ) -> JSValue { - let key = get_addr_info_request::PendingCacheKey::init(query_init); + let key = PendingCacheKey::init_query(query_init); let cache = this.get_or_put_into_pending_cache(&key, PendingCacheField::PendingHostCacheNative); @@ -418,7 +418,7 @@ pub(super) mod lib_uv_backend { query: GetAddrInfo, global_this: &JSGlobalObject, ) -> JsResult { - let key = get_addr_info_request::PendingCacheKey::init(&query); + let key = PendingCacheKey::init_query(&query); let cache = this.get_or_put_into_pending_cache(&key, PendingCacheField::PendingHostCacheNative); @@ -600,34 +600,97 @@ pub struct ResolveInfoRequest { pub tail: *mut CAresLookup, // INTRUSIVE — points at `head` or last appended node } -pub mod resolve_info_request { - use super::*; - - pub struct PendingCacheKey { - pub hash: u64, - pub len: u16, - pub name: Box<[u8]>, - pub lookup: *mut ResolveInfoRequest, - } +/// Request types holding an intrusive `head`/`tail` list of lookup nodes, so the +/// shared `PendingCacheKey` can append a waiter while the request is in flight. +pub trait HasTail { + type Node; + /// Append `node` after the current tail and advance `tail`. + /// + /// # Safety + /// `this` and its current `tail` must point at live nodes. + unsafe fn append_node(this: *mut Self, node: *mut Self::Node); +} - impl PendingCacheKey { - pub(crate) fn append(&mut self, cares_lookup: *mut CAresLookup) { - // SAFETY: lookup/tail are valid while request is in the pending cache +macro_rules! impl_has_tail { + (<$T:ident: $bound:path> $req:ty => $node:ty) => { + impl<$T: $bound> HasTail for $req { impl_has_tail!(@body $node); } + }; + ($req:ty => $node:ty) => { + impl HasTail for $req { impl_has_tail!(@body $node); } + }; + (@body $node:ty) => { + type Node = $node; + unsafe fn append_node(this: *mut Self, node: *mut Self::Node) { + // SAFETY: fn contract — `this` and its current `tail` are live. unsafe { - let tail = (*self.lookup).tail; - (*tail).next = NonNull::new(cares_lookup); - (*self.lookup).tail = cares_lookup; + let tail = (*this).tail; + (*tail).next = NonNull::new(node); + (*this).tail = node; } } + }; +} - pub(crate) fn init(name: &[u8]) -> Self { - let hash = wyhash(name); - Self { - hash, - len: name.len() as u16, - name: Box::<[u8]>::from(name), - lookup: ptr::null_mut(), - } +impl_has_tail!( ResolveInfoRequest => CAresLookup); + +/// Pending-cache slot key: dedupes in-flight DNS requests by `{hash, len, name}` +/// and points at the request whose intrusive list collects waiting lookups. +pub struct PendingCacheKey { + pub hash: u64, + pub len: u16, + pub name: Box<[u8]>, + pub lookup: *mut Req, +} + +/// Request types whose pending-cache key hashes only the lookup name. +/// `GetAddrInfoRequest` is deliberately excluded: its keys must be built with +/// [`PendingCacheKey::init_query`], which hashes `port` + `options` + `name`. +pub trait NameKeyed: HasTail {} + +impl NameKeyed for ResolveInfoRequest {} +impl NameKeyed for GetHostByAddrInfoRequest {} +impl NameKeyed for GetNameInfoRequest {} + +impl PendingCacheKey { + pub(crate) fn append(&mut self, node: *mut Req::Node) { + // SAFETY: lookup/tail are valid while request is in the pending cache + unsafe { Req::append_node(self.lookup, node) } + } + + /// `{ hash, len, name, lookup: null }` copy for `HiveArray::get_init`. + /// `lookup` is filled in later by `*Request::init` once the request has + /// been heap-allocated; until then it is a defined null rather than uninit + /// garbage, so the `iter_set` loop in `get_or_put_into_pending_cache` can + /// safely materialise `&mut PendingCacheKey` over the slot. + pub(crate) fn unlinked(&self) -> Self { + Self { + hash: self.hash, + len: self.len, + name: self.name.clone(), + lookup: ptr::null_mut(), + } + } +} + +impl PendingCacheKey { + pub(crate) fn init(name: &[u8]) -> Self { + Self { + hash: wyhash(name), + len: name.len() as u16, + name: Box::<[u8]>::from(name), + lookup: ptr::null_mut(), + } + } +} + +impl PendingCacheKey { + /// addr-info keys hash `port` + `options` + `name`, not just the name bytes. + pub(crate) fn init_query(query: &GetAddrInfo) -> Self { + Self { + hash: query.hash(), + len: query.name.len() as u16, + name: query.name.clone(), + lookup: ptr::null_mut(), } } } @@ -740,37 +803,7 @@ pub struct GetHostByAddrInfoRequest { pub tail: *mut CAresReverse, // INTRUSIVE } -pub mod get_host_by_addr_info_request { - use super::*; - - pub struct PendingCacheKey { - pub hash: u64, - pub len: u16, - pub name: Box<[u8]>, - pub lookup: *mut GetHostByAddrInfoRequest, - } - - impl PendingCacheKey { - pub(crate) fn append(&mut self, cares_lookup: *mut CAresReverse) { - // SAFETY: lookup/tail are valid while request is in the pending cache - unsafe { - let tail = (*self.lookup).tail; - (*tail).next = NonNull::new(cares_lookup); - (*self.lookup).tail = cares_lookup; - } - } - - pub(crate) fn init(name: &[u8]) -> Self { - let hash = wyhash(name); - Self { - hash, - len: name.len() as u16, - name: Box::<[u8]>::from(name), - lookup: ptr::null_mut(), - } - } - } -} +impl_has_tail!(GetHostByAddrInfoRequest => CAresReverse); impl GetHostByAddrInfoRequest { /// Reverse lookups always cache through `pending_addr_cache_cares`, so no @@ -991,37 +1024,7 @@ pub struct GetNameInfoRequest { pub tail: *mut CAresNameInfo, // INTRUSIVE } -pub mod get_name_info_request { - use super::*; - - pub struct PendingCacheKey { - pub hash: u64, - pub len: u16, - pub name: Box<[u8]>, - pub lookup: *mut GetNameInfoRequest, - } - - impl PendingCacheKey { - pub(crate) fn append(&mut self, cares_lookup: *mut CAresNameInfo) { - // SAFETY: lookup/tail are valid while request is in the pending cache - unsafe { - let tail = (*self.lookup).tail; - (*tail).next = NonNull::new(cares_lookup); - (*self.lookup).tail = cares_lookup; - } - } - - pub(crate) fn init(name: &[u8]) -> Self { - let hash = wyhash(name); - Self { - hash, - len: name.len() as u16, - name: Box::<[u8]>::from(name), - lookup: ptr::null_mut(), - } - } - } -} +impl_has_tail!(GetNameInfoRequest => CAresNameInfo); impl GetNameInfoRequest { pub(crate) fn init( @@ -1134,6 +1137,8 @@ pub struct GetAddrInfoRequest { pub task: thread_pool::Task, } +impl_has_tail!(GetAddrInfoRequest => DNSLookup); + pub mod get_addr_info_request { use super::*; @@ -1141,33 +1146,6 @@ pub mod get_addr_info_request { /// on the work pool, then re-enters the JS thread via `then`. pub type Task = jsc::work_task::WorkTask; - pub struct PendingCacheKey { - pub hash: u64, - pub len: u16, - pub name: Box<[u8]>, - pub lookup: *mut GetAddrInfoRequest, - } - - impl PendingCacheKey { - pub(crate) fn append(&mut self, dns_lookup: *mut DNSLookup) { - // SAFETY: `lookup`/`tail` are valid while the request sits in the pending cache. - unsafe { - let tail = (*self.lookup).tail; - (*tail).next = NonNull::new(dns_lookup); - (*self.lookup).tail = dns_lookup; - } - } - - pub(crate) fn init(query: &GetAddrInfo) -> Self { - Self { - hash: query.hash(), - len: query.name.len() as u16, - name: query.name.clone(), - lookup: ptr::null_mut(), - } - } - } - #[derive(Default)] pub struct BackendLibInfo { /// OWNED hive slot from `FilePoll::init` (returned via `FilePoll::deinit`, @@ -3610,28 +3588,22 @@ hostent_ttls_newtype!( parse_aaaa ); -pub type PendingCache = HiveArray; -type SrvPendingCache = - HiveArray, 32>; -type SoaPendingCache = - HiveArray, 32>; -type TxtPendingCache = - HiveArray, 32>; -type NaptrPendingCache = - HiveArray, 32>; -type MxPendingCache = - HiveArray, 32>; -type CaaPendingCache = - HiveArray, 32>; -type NSPendingCache = HiveArray, 32>; -type PtrPendingCache = HiveArray, 32>; -type CnamePendingCache = HiveArray, 32>; -type APendingCache = HiveArray, 32>; -type AAAAPendingCache = HiveArray, 32>; -type AnyPendingCache = - HiveArray, 32>; -type AddrPendingCache = HiveArray; -type NameInfoPendingCache = HiveArray; +pub type PendingCache = HiveArray, 32>; +type ResolvePendingCache = HiveArray>, 32>; +type SrvPendingCache = ResolvePendingCache; +type SoaPendingCache = ResolvePendingCache; +type TxtPendingCache = ResolvePendingCache; +type NaptrPendingCache = ResolvePendingCache; +type MxPendingCache = ResolvePendingCache; +type CaaPendingCache = ResolvePendingCache; +type NSPendingCache = ResolvePendingCache; +type PtrPendingCache = ResolvePendingCache; +type CnamePendingCache = ResolvePendingCache; +type APendingCache = ResolvePendingCache; +type AAAAPendingCache = ResolvePendingCache; +type AnyPendingCache = ResolvePendingCache; +type AddrPendingCache = HiveArray, 32>; +type NameInfoPendingCache = HiveArray, 32>; #[cfg(windows)] type PollType = UvDnsPoll; @@ -3732,18 +3704,11 @@ impl UvDnsPoll { } } -#[derive(Clone, Copy)] -pub enum CacheHit { - Inflight(*mut get_addr_info_request::PendingCacheKey), // BORROW_FIELD into resolver buffer - New(*mut get_addr_info_request::PendingCacheKey), // BORROW_FIELD into resolver buffer - Disabled, -} +pub type CacheHit = LookupCacheHit; pub enum LookupCacheHit { - // The request type is threaded via `R`; `PendingCacheKey` resolves - // through `HasPendingCacheKey`. - Inflight(*mut R::PendingCacheKey), // BORROW_FIELD - New(*mut R::PendingCacheKey), // BORROW_FIELD + Inflight(*mut PendingCacheKey), // BORROW_FIELD into resolver buffer + New(*mut PendingCacheKey), // BORROW_FIELD into resolver buffer Disabled, } @@ -3754,11 +3719,9 @@ impl Clone for LookupCacheHit { } impl Copy for LookupCacheHit {} -/// Associates a request type with its `PendingCacheKey` and the matching `HiveArray` +/// Associates a request type with the matching pending-cache `HiveArray` /// field on `Resolver`. -pub trait HasPendingCacheKey { - type PendingCacheKey; - +pub trait HasPendingCacheKey: HasTail + Sized { /// Return the per-request-type pending HiveArray field on `Resolver`. /// `field` is the runtime tag selecting which field (some request types are reachable /// via more than one field, e.g. `pending_host_cache_{cares,native}`). @@ -3770,122 +3733,50 @@ pub trait HasPendingCacheKey { fn pending_cache( resolver: &Resolver, field: PendingCacheField, - ) -> &mut HiveArray; - - /// `key.hash` — all `PendingCacheKey` shapes carry `{ hash: u64, len: u16, lookup: *mut _ }`. - fn key_hash(key: &Self::PendingCacheKey) -> u64; - /// `key.len` - fn key_len(key: &Self::PendingCacheKey) -> u16; - fn key_name(key: &Self::PendingCacheKey) -> &[u8]; - /// Construct a fully-initialized `PendingCacheKey { hash, len, lookup: null }` - /// for `HiveArray::get_init`. `lookup` is filled in later by `*Request::init` - /// once the request has been heap-allocated; until then it is a defined null - /// rather than uninit garbage, so the `iter_set` loop in - /// `get_or_put_into_resolve_pending_cache` can safely materialise - /// `&mut PendingCacheKey` over the slot. - fn key_new(key: &Self::PendingCacheKey) -> Self::PendingCacheKey; + ) -> &mut HiveArray, 32>; } impl HasPendingCacheKey for ResolveInfoRequest { - type PendingCacheKey = resolve_info_request::PendingCacheKey; - #[inline] fn pending_cache( resolver: &Resolver, field: PendingCacheField, - ) -> &mut HiveArray { + ) -> &mut HiveArray, 32> { resolver.pending_cache_for::(field) } - #[inline] - fn key_hash(key: &Self::PendingCacheKey) -> u64 { - key.hash - } - #[inline] - fn key_len(key: &Self::PendingCacheKey) -> u16 { - key.len - } - #[inline] - fn key_name(key: &Self::PendingCacheKey) -> &[u8] { - &key.name - } - #[inline] - fn key_new(key: &Self::PendingCacheKey) -> Self::PendingCacheKey { - resolve_info_request::PendingCacheKey { - hash: key.hash, - len: key.len, - name: key.name.clone(), - lookup: ptr::null_mut(), - } - } } impl HasPendingCacheKey for GetHostByAddrInfoRequest { - type PendingCacheKey = get_host_by_addr_info_request::PendingCacheKey; - #[inline] fn pending_cache( resolver: &Resolver, _field: PendingCacheField, - ) -> &mut HiveArray { + ) -> &mut HiveArray, 32> { // SAFETY: see `HasPendingCacheKey::pending_cache` doc — short, // non-reentrant borrow on the single JS thread. unsafe { resolver.pending_addr_cache_cares.get_mut() } } - #[inline] - fn key_hash(key: &Self::PendingCacheKey) -> u64 { - key.hash - } - #[inline] - fn key_len(key: &Self::PendingCacheKey) -> u16 { - key.len - } - #[inline] - fn key_name(key: &Self::PendingCacheKey) -> &[u8] { - &key.name - } - #[inline] - fn key_new(key: &Self::PendingCacheKey) -> Self::PendingCacheKey { - get_host_by_addr_info_request::PendingCacheKey { - hash: key.hash, - len: key.len, - name: key.name.clone(), - lookup: ptr::null_mut(), - } - } } impl HasPendingCacheKey for GetNameInfoRequest { - type PendingCacheKey = get_name_info_request::PendingCacheKey; - #[inline] fn pending_cache( resolver: &Resolver, _field: PendingCacheField, - ) -> &mut HiveArray { + ) -> &mut HiveArray, 32> { // SAFETY: see `HasPendingCacheKey::pending_cache` doc — short, // non-reentrant borrow on the single JS thread. unsafe { resolver.pending_nameinfo_cache_cares.get_mut() } } +} + +impl HasPendingCacheKey for GetAddrInfoRequest { #[inline] - fn key_hash(key: &Self::PendingCacheKey) -> u64 { - key.hash - } - #[inline] - fn key_len(key: &Self::PendingCacheKey) -> u16 { - key.len - } - #[inline] - fn key_name(key: &Self::PendingCacheKey) -> &[u8] { - &key.name - } - #[inline] - fn key_new(key: &Self::PendingCacheKey) -> Self::PendingCacheKey { - get_name_info_request::PendingCacheKey { - hash: key.hash, - len: key.len, - name: key.name.clone(), - lookup: ptr::null_mut(), - } + fn pending_cache( + resolver: &Resolver, + field: PendingCacheField, + ) -> &mut HiveArray, 32> { + resolver.pending_host_cache(field) } } @@ -3946,6 +3837,110 @@ impl RecordType { pub const DEFAULT: Self = RecordType::A; } +/// Intrusive pending-chain node shared by the `drain_pending_*` family. +trait PendingChainNode: Sized { + fn chain_next(&self) -> Option>; + fn chain_global(&self) -> &JSGlobalObject; +} + +macro_rules! impl_pending_chain_node { + ($($node:ty),* $(,)?) => {$( + impl PendingChainNode for $node { + #[inline] + fn chain_next(&self) -> Option> { + self.next + } + #[inline] + fn chain_global(&self) -> &JSGlobalObject { + self.global_this() + } + } + )*}; +} +impl_pending_chain_node!(DNSLookup, CAresReverse, CAresNameInfo); + +impl PendingChainNode for CAresLookup { + #[inline] + fn chain_next(&self) -> Option> { + self.next + } + #[inline] + fn chain_global(&self) -> &JSGlobalObject { + self.global_this() + } +} + +/// Error-arm skeleton shared by the `drain_pending_*` family: hand the +/// in-place chain head to `process`, free the boxed request via +/// `consume_head`, then walk the remaining (individually boxed) tail nodes. +/// +/// SAFETY: `head` must point at the intrusive head embedded in the live, +/// heap-allocated request held by the pending-cache slot. `consume_head` must +/// consume exactly that request (via `heap::take`) without touching the tail +/// nodes, and `process` must consume each node it is handed (the per-type +/// `process_*` contract). +unsafe fn drain_chain_err( + head: *mut Node, + mut process: impl FnMut(*mut Node), + consume_head: impl FnOnce(), +) { + // SAFETY: see fn contract — each node's `next` is read before the node is + // consumed. + unsafe { + let mut pending = (*head).chain_next(); + process(head); + consume_head(); + + while let Some(value) = pending { + pending = (*value.as_ptr()).chain_next(); + process(value.as_ptr()); + } + } +} + +/// Success-arm skeleton shared by the `drain_pending_*` family. `array` is +/// the JS response already materialized for `prev_global` (the head's +/// global); `to_js` re-materializes it whenever a tail node belongs to a +/// different global. `ensure_still_alive` brackets every `on_complete` so the +/// conservative stack scan keeps `array` rooted across the completion +/// callbacks. +/// +/// SAFETY: same contract as [`drain_chain_err`], with `on_complete` consuming +/// each node it is handed. Additionally, `to_js` must not append to or +/// consume chain nodes: each node's `next` is snapshotted only as the walk +/// reaches it, after earlier `to_js`/`on_complete` calls have run. +unsafe fn drain_chain_ok<'a, Node: PendingChainNode + 'a>( + head: *mut Node, + mut array: JSValue, + mut prev_global: &'a JSGlobalObject, + mut to_js: impl FnMut(&JSGlobalObject) -> JSValue, + mut on_complete: impl FnMut(*mut Node, JSValue), + consume_head: impl FnOnce(), +) { + // SAFETY: see fn contract — each node's `next` is read before the node is + // consumed. + unsafe { + let mut pending = (*head).chain_next(); + array.ensure_still_alive(); + on_complete(head, array); + consume_head(); + array.ensure_still_alive(); + + while let Some(value) = pending { + let new_global = (*value.as_ptr()).chain_global(); + if !core::ptr::eq(prev_global, new_global) { + array = to_js(new_global); + prev_global = new_global; + } + pending = (*value.as_ptr()).chain_next(); + + array.ensure_still_alive(); + on_complete(value.as_ptr(), array); + array.ensure_still_alive(); + } + } +} + impl Resolver { /// Dereference the back-pointer to the VirtualMachine. /// SAFETY: VirtualMachine outlives the Resolver (BACKREF, see field decl). @@ -4232,7 +4227,7 @@ impl Resolver { /// Dispatch to a typed ResolveInfoRequest cache by record type. // Each per-record cache is a distinct monomorphization of - // `HiveArray, 32>`; `PendingCacheKey` is + // `ResolvePendingCache<_>`; `PendingCacheKey>` is // layout-identical for all `T` (only the `*mut ResolveInfoRequest` payload's pointee // type differs), so reinterpreting the field reference at the caller's `T` is sound when // `T::CACHE_FIELD` selects the matching field. @@ -4240,21 +4235,16 @@ impl Resolver { fn pending_cache_for( &self, _field: PendingCacheField, - ) -> &mut HiveArray, 32> { + ) -> &mut ResolvePendingCache { macro_rules! field { ($f:ident) => { // SAFETY: the matched arm guarantees `self.$f` *is* - // `JsCell, 32>>` for this `T::CACHE_FIELD`; + // `JsCell>` for this `T::CACHE_FIELD`; // the cast is an identity transmute (same layout, same lifetime). // R-2: `JsCell::as_ptr` projects `&mut` from `&self`; caller // holds the borrow only for a short, non-reentrant window // (see `pending_host_cache` doc). - unsafe { - &mut *self - .$f - .as_ptr() - .cast::, 32>>() - } + unsafe { &mut *self.$f.as_ptr().cast::>() } }; } match T::CACHE_FIELD { @@ -4285,24 +4275,24 @@ impl Resolver { &self, index: u8, field: PendingCacheField, - ) -> get_addr_info_request::PendingCacheKey { + ) -> PendingCacheKey { let cache = self.pending_host_cache(field); - // SAFETY: slot at `index` was alloc'd by `get_or_put_into_resolve_pending_cache`. + // SAFETY: slot at `index` was alloc'd by `get_or_put_into_pending_cache`. unsafe { cache.box_at(index as usize) } .expect("pending DNS slot") .into_inner() } - fn get_key_addr(&self, index: u8) -> get_host_by_addr_info_request::PendingCacheKey { + fn get_key_addr(&self, index: u8) -> PendingCacheKey { self.pending_addr_cache_cares.with_mut(|cache| { - // SAFETY: slot at `index` was alloc'd by `get_or_put_into_resolve_pending_cache`. + // SAFETY: slot at `index` was alloc'd by `get_or_put_into_pending_cache`. unsafe { cache.box_at(index as usize) } .expect("pending DNS slot") .into_inner() }) } - fn get_key_nameinfo(&self, index: u8) -> get_name_info_request::PendingCacheKey { + fn get_key_nameinfo(&self, index: u8) -> PendingCacheKey { self.pending_nameinfo_cache_cares.with_mut(|cache| { - // SAFETY: slot at `index` was alloc'd by `get_or_put_into_resolve_pending_cache`. + // SAFETY: slot at `index` was alloc'd by `get_or_put_into_pending_cache`. unsafe { cache.box_at(index as usize) } .expect("pending DNS slot") .into_inner() @@ -4322,63 +4312,46 @@ impl Resolver { let key = { let cache = self.pending_cache_for::(T::CACHE_FIELD); - // SAFETY: slot at `index` was alloc'd by `get_or_put_into_resolve_pending_cache`. + // SAFETY: slot at `index` was alloc'd by `get_or_put_into_pending_cache`. unsafe { cache.box_at(index as usize) } .expect("pending DNS slot") .into_inner() }; - let Some(addr) = result else { - // SAFETY: `key.lookup` is the heap-allocated request stored in the - // pending-cache slot; consumed via `heap::take` below. - unsafe { - let mut pending = (*key.lookup).head.next; - CAresLookup::::process_resolve( - ptr::addr_of_mut!((*key.lookup).head), - err, - timeout, - None, + // SAFETY: `key.lookup` is the heap-allocated request stored in the + // pending-cache slot; consumed via `heap::take` in `consume_head`. + // `addr` is the c-ares-allocated reply freed by `_free_addr` below. + unsafe { + let head = ptr::addr_of_mut!((*key.lookup).head); + let consume_head = || drop(bun_core::heap::take(key.lookup)); + + let Some(addr) = result else { + drain_chain_err( + head, + |node| CAresLookup::::process_resolve(node, err, timeout, None), + consume_head, ); - drop(bun_core::heap::take(key.lookup)); - - while let Some(value) = pending { - pending = (*value.as_ptr()).next; - CAresLookup::::process_resolve(value.as_ptr(), err, timeout, None); - } - } - return; - }; + return; + }; - // SAFETY: `key.lookup` is the heap-allocated request stored in the pending-cache - // slot; `addr` is the c-ares-allocated reply freed by `_free_addr` below. - unsafe { - let mut pending = (*key.lookup).head.next; - let mut prev_global = (*key.lookup).head.global_this(); - let mut array = (*addr) - .to_js_response(prev_global, T::TYPE_NAME) + let head_global = (*head).global_this(); + let array = (*addr) + .to_js_response(head_global, T::TYPE_NAME) .unwrap_or(JSValue::ZERO); // TODO: properly propagate exception upwards // SAFETY: addr is the c-ares-allocated reply; freed once after all consumers run. let _free_addr = scopeguard::guard(addr, |a| T::destroy(a)); - array.ensure_still_alive(); - CAresLookup::::on_complete(ptr::addr_of_mut!((*key.lookup).head), array); - drop(bun_core::heap::take(key.lookup)); - - array.ensure_still_alive(); - - while let Some(value) = pending { - let new_global = (*value.as_ptr()).global_this(); - if !core::ptr::eq(prev_global, new_global) { - array = (*addr) - .to_js_response(new_global, T::TYPE_NAME) - .unwrap_or(JSValue::ZERO); // TODO: properly propagate exception upwards - prev_global = new_global; - } - pending = (*value.as_ptr()).next; - - array.ensure_still_alive(); - CAresLookup::::on_complete(value.as_ptr(), array); - array.ensure_still_alive(); - } + drain_chain_ok( + head, + array, + head_global, + |global| { + (*addr) + .to_js_response(global, T::TYPE_NAME) + .unwrap_or(JSValue::ZERO) // TODO: properly propagate exception upwards + }, + |node, value| CAresLookup::::on_complete(node, value), + consume_head, + ); } } @@ -4394,56 +4367,39 @@ impl Resolver { // SAFETY: `self` is the live heap allocation; ref_scope keeps count > 0 across re-entrant callbacks. let _g = unsafe { Self::ref_scope(self.as_ctx_ptr()) }; - let Some(addr) = result else { - // SAFETY: `key.lookup` is the heap-allocated request stored in the - // pending-cache slot; consumed via `heap::take` below. - unsafe { - let mut pending = (*key.lookup).head.next; - DNSLookup::process_get_addr_info( - ptr::addr_of_mut!((*key.lookup).head), - err, - timeout, - None, + // SAFETY: `key.lookup` is the heap-allocated request stored in the + // pending-cache slot; consumed via `heap::take` in `consume_head`. + // `addr` is the c-ares-allocated AddrInfo freed by `_free_addr` below. + unsafe { + let head = ptr::addr_of_mut!((*key.lookup).head); + let consume_head = || drop(bun_core::heap::take(key.lookup)); + + let Some(addr) = result else { + drain_chain_err( + head, + |node| DNSLookup::process_get_addr_info(node, err, timeout, None), + consume_head, ); - drop(bun_core::heap::take(key.lookup)); - - while let Some(value) = pending { - pending = (*value.as_ptr()).next; - DNSLookup::process_get_addr_info(value.as_ptr(), err, timeout, None); - } - } - return; - }; + return; + }; - // SAFETY: `key.lookup` is the heap-allocated request stored in the pending-cache - // slot; `addr` is the c-ares-allocated AddrInfo freed by `_free_addr` below. - unsafe { - let mut pending = (*key.lookup).head.next; - let mut prev_global = (*key.lookup).head.global_this(); - let mut array = super::cares_jsc::addr_info_to_js_array(&mut *addr, prev_global) + let head_global = (*head).global_this(); + let array = super::cares_jsc::addr_info_to_js_array(&mut *addr, head_global) .unwrap_or(JSValue::ZERO); // TODO: properly propagate exception upwards // SAFETY: addr is the c-ares-allocated AddrInfo; freed once after all consumers run. // Move the raw pointer into the guard so the loop body can keep borrowing `*addr`. let _free_addr = scopeguard::guard(addr, |a| c_ares::AddrInfo::destroy(a)); - array.ensure_still_alive(); - DNSLookup::on_complete_with_array(ptr::addr_of_mut!((*key.lookup).head), array); - drop(bun_core::heap::take(key.lookup)); - - array.ensure_still_alive(); - - while let Some(value) = pending { - let new_global = (*value.as_ptr()).global_this(); - if !core::ptr::eq(prev_global, new_global) { - array = super::cares_jsc::addr_info_to_js_array(&mut *addr, new_global) - .unwrap_or(JSValue::ZERO); // TODO: properly propagate exception upwards - prev_global = new_global; - } - pending = (*value.as_ptr()).next; - - array.ensure_still_alive(); - DNSLookup::on_complete_with_array(value.as_ptr(), array); - array.ensure_still_alive(); - } + drain_chain_ok( + head, + array, + head_global, + |global| { + super::cares_jsc::addr_info_to_js_array(&mut *addr, global) + .unwrap_or(JSValue::ZERO) // TODO: properly propagate exception upwards + }, + |node, value| DNSLookup::on_complete_with_array(node, value), + consume_head, + ); } } @@ -4460,7 +4416,7 @@ impl Resolver { // SAFETY: `self` is the live heap allocation; ref_scope keeps count > 0 across re-entrant callbacks. let _g = unsafe { Self::ref_scope(self.as_ctx_ptr()) }; - let mut array: JSValue = match super::options_jsc::result_any_to_js(result, global_object) + let array: JSValue = match super::options_jsc::result_any_to_js(result, global_object) .unwrap_or(None) { // TODO: properly propagate exception upwards @@ -4489,32 +4445,21 @@ impl Resolver { } }; // SAFETY: `key.lookup` is the heap-allocated request stored in the - // pending-cache slot; consumed via `heap::take` below. + // pending-cache slot; consumed via `heap::take` in `consume_head`. unsafe { - let mut pending = (*key.lookup).head.next; - let mut prev_global = (*key.lookup).head.global_this(); - - { - array.ensure_still_alive(); - DNSLookup::on_complete_with_array(ptr::addr_of_mut!((*key.lookup).head), array); - drop(bun_core::heap::take(key.lookup)); - array.ensure_still_alive(); - } - - while let Some(value) = pending { - let new_global = (*value.as_ptr()).global_this(); - pending = (*value.as_ptr()).next; - if !core::ptr::eq(prev_global, new_global) { - array = super::options_jsc::result_any_to_js(result, new_global) + let head = ptr::addr_of_mut!((*key.lookup).head); + drain_chain_ok( + head, + array, + (*head).global_this(), + |global| { + super::options_jsc::result_any_to_js(result, global) .unwrap_or(None) - .unwrap(); // TODO: properly propagate exception upwards - prev_global = new_global; - } - - array.ensure_still_alive(); - DNSLookup::on_complete_with_array(value.as_ptr(), array); - array.ensure_still_alive(); - } + .unwrap() // TODO: properly propagate exception upwards + }, + |node, value| DNSLookup::on_complete_with_array(node, value), + || drop(bun_core::heap::take(key.lookup)), + ); } } @@ -4530,56 +4475,39 @@ impl Resolver { // SAFETY: `self` is the live heap allocation; ref_scope keeps count > 0 across re-entrant callbacks. let _g = unsafe { Self::ref_scope(self.as_ctx_ptr()) }; - let Some(addr) = result else { - // SAFETY: `key.lookup` is the heap-allocated request stored in the - // pending-cache slot; consumed via `heap::take` below. - unsafe { - let mut pending = (*key.lookup).head.next; - CAresReverse::process_resolve( - ptr::addr_of_mut!((*key.lookup).head), - err, - timeout, - None, + // SAFETY: `key.lookup` is the heap-allocated request stored in the + // pending-cache slot; consumed via `heap::take` in `consume_head`. + // `addr` is the c-ares-owned hostent (freed by c-ares after the callback). + unsafe { + let head = ptr::addr_of_mut!((*key.lookup).head); + let consume_head = || drop(bun_core::heap::take(key.lookup)); + + let Some(addr) = result else { + drain_chain_err( + head, + |node| CAresReverse::process_resolve(node, err, timeout, None), + consume_head, ); - drop(bun_core::heap::take(key.lookup)); - - while let Some(value) = pending { - pending = (*value.as_ptr()).next; - CAresReverse::process_resolve(value.as_ptr(), err, timeout, None); - } - } - return; - }; + return; + }; - // SAFETY: `key.lookup` is the heap-allocated request stored in the pending-cache - // slot; `addr` is the c-ares-owned hostent (freed by c-ares after the callback). - unsafe { - let mut pending = (*key.lookup).head.next; - let mut prev_global = (*key.lookup).head.global_this(); // The callback need not and should not attempt to free the memory // pointed to by hostent; the ares library will free it when the // callback returns. - let mut array = super::cares_jsc::hostent_to_js_response(&mut *addr, prev_global, b"") + let head_global = (*head).global_this(); + let array = super::cares_jsc::hostent_to_js_response(&mut *addr, head_global, b"") .unwrap_or(JSValue::ZERO); // TODO: properly propagate exception upwards - array.ensure_still_alive(); - CAresReverse::on_complete(ptr::addr_of_mut!((*key.lookup).head), array); - drop(bun_core::heap::take(key.lookup)); - - array.ensure_still_alive(); - - while let Some(value) = pending { - let new_global = (*value.as_ptr()).global_this(); - if !core::ptr::eq(prev_global, new_global) { - array = super::cares_jsc::hostent_to_js_response(&mut *addr, new_global, b"") - .unwrap_or(JSValue::ZERO); // TODO: properly propagate exception upwards - prev_global = new_global; - } - pending = (*value.as_ptr()).next; - - array.ensure_still_alive(); - CAresReverse::on_complete(value.as_ptr(), array); - array.ensure_still_alive(); - } + drain_chain_ok( + head, + array, + head_global, + |global| { + super::cares_jsc::hostent_to_js_response(&mut *addr, global, b"") + .unwrap_or(JSValue::ZERO) // TODO: properly propagate exception upwards + }, + |node, value| CAresReverse::on_complete(node, value), + consume_head, + ); } } @@ -4595,60 +4523,41 @@ impl Resolver { // SAFETY: `self` is the live heap allocation; ref_scope keeps count > 0 across re-entrant callbacks. let _g = unsafe { Self::ref_scope(self.as_ctx_ptr()) }; - let Some(mut name_info) = result else { - // SAFETY: `key.lookup` is the heap-allocated request stored in the - // pending-cache slot; consumed via `heap::take` below. - unsafe { - let mut pending = (*key.lookup).head.next; - CAresNameInfo::process_resolve( - ptr::addr_of_mut!((*key.lookup).head), - err, - timeout, - None, - ); - drop(bun_core::heap::take(key.lookup)); - - while let Some(value) = pending { - pending = (*value.as_ptr()).next; - CAresNameInfo::process_resolve(value.as_ptr(), err, timeout, None); - } - } - return; - }; - // SAFETY: `key.lookup` is the heap-allocated request stored in the - // pending-cache slot; consumed via `heap::take` below. + // pending-cache slot; consumed via `heap::take` in `consume_head`. unsafe { - let mut pending = (*key.lookup).head.next; - let mut prev_global = (*key.lookup).head.global_this(); + let head = ptr::addr_of_mut!((*key.lookup).head); + let consume_head = || drop(bun_core::heap::take(key.lookup)); + + let Some(mut name_info) = result else { + drain_chain_err( + head, + |node| CAresNameInfo::process_resolve(node, err, timeout, None), + consume_head, + ); + return; + }; - let mut array = super::cares_jsc::nameinfo_to_js_response(&mut name_info, prev_global) + let head_global = (*head).global_this(); + let array = super::cares_jsc::nameinfo_to_js_response(&mut name_info, head_global) .unwrap_or(JSValue::ZERO); // TODO: properly propagate exception upwards - array.ensure_still_alive(); - CAresNameInfo::on_complete(ptr::addr_of_mut!((*key.lookup).head), array); - drop(bun_core::heap::take(key.lookup)); - - array.ensure_still_alive(); - - while let Some(value) = pending { - let new_global = (*value.as_ptr()).global_this(); - if !core::ptr::eq(prev_global, new_global) { - array = super::cares_jsc::nameinfo_to_js_response(&mut name_info, new_global) - .unwrap_or(JSValue::ZERO); // TODO: properly propagate exception upwards - prev_global = new_global; - } - pending = (*value.as_ptr()).next; - - array.ensure_still_alive(); - CAresNameInfo::on_complete(value.as_ptr(), array); - array.ensure_still_alive(); - } + drain_chain_ok( + head, + array, + head_global, + |global| { + super::cares_jsc::nameinfo_to_js_response(&mut name_info, global) + .unwrap_or(JSValue::ZERO) // TODO: properly propagate exception upwards + }, + |node, value| CAresNameInfo::on_complete(node, value), + consume_head, + ); } } - pub fn get_or_put_into_resolve_pending_cache( + pub fn get_or_put_into_pending_cache( &self, - key: &R::PendingCacheKey, + key: &PendingCacheKey, field: PendingCacheField, ) -> LookupCacheHit { // Dispatch via `HasPendingCacheKey::pending_cache`; the body is @@ -4659,49 +4568,18 @@ impl Resolver { while let Some(index) = inflight_iter.next() { // SAFETY: `used` bit is set ⇒ slot was initialized. let entry = unsafe { &mut *cache.ptr_at(index) }; - if R::key_hash(entry) == R::key_hash(key) - && R::key_len(entry) == R::key_len(key) - && R::key_name(entry) == R::key_name(key) - { + if entry.hash == key.hash && entry.len == key.len && entry.name == key.name { return LookupCacheHit::Inflight(std::ptr::from_mut(entry)); } } - if let Some(new) = cache.get_init(R::key_new(key)) { + if let Some(new) = cache.get_init(key.unlinked()) { return LookupCacheHit::New(new.as_ptr()); } LookupCacheHit::Disabled } - pub fn get_or_put_into_pending_cache( - &self, - key: &get_addr_info_request::PendingCacheKey, - field: PendingCacheField, - ) -> CacheHit { - let cache = self.pending_host_cache(field); - let mut inflight_iter = cache.used.iter_set(); - - while let Some(index) = inflight_iter.next() { - // SAFETY: `used` bit is set ⇒ slot was initialized. - let entry = unsafe { &mut *cache.ptr_at(index) }; - if entry.hash == key.hash && entry.len == key.len && entry.name == key.name { - return CacheHit::Inflight(std::ptr::from_mut(entry)); - } - } - - if let Some(new) = cache.get_init(get_addr_info_request::PendingCacheKey { - hash: key.hash, - len: key.len, - name: key.name.clone(), - lookup: ptr::null_mut(), - }) { - return CacheHit::New(new.as_ptr()); - } - - CacheHit::Disabled - } - pub fn get_channel(&self) -> ChannelResult<'_> { if self.channel.get().is_none() { let opts = self.options.get(); @@ -5097,8 +4975,8 @@ impl Resolver { } }; - let key = get_host_by_addr_info_request::PendingCacheKey::init(ip); - let cache = self.get_or_put_into_resolve_pending_cache::( + let key = PendingCacheKey::::init(ip); + let cache = self.get_or_put_into_pending_cache::( &key, PendingCacheField::PendingAddrCacheCares, ); @@ -5390,10 +5268,9 @@ impl Resolver { let cache_field = T::CACHE_FIELD; // "pending_{TYPE_NAME}_cache_cares" - let key = resolve_info_request::PendingCacheKey::::init(name); + let key = PendingCacheKey::>::init(name); - let cache = - self.get_or_put_into_resolve_pending_cache::>(&key, cache_field); + let cache = self.get_or_put_into_pending_cache::>(&key, cache_field); if let LookupCacheHit::Inflight(inflight) = cache { // CAresLookup will have the name ownership let cares_lookup = CAresLookup::::init(Some(self.as_ctx_ptr()), global_this, name); @@ -5449,7 +5326,7 @@ impl Resolver { } }; - let key = get_addr_info_request::PendingCacheKey::init(query); + let key = PendingCacheKey::init_query(query); let cache = self.get_or_put_into_pending_cache(&key, PendingCacheField::PendingHostCacheCares); @@ -5970,8 +5847,8 @@ impl Resolver { } let cache_name: Box<[u8]> = cache_name.into_boxed_slice(); - let key = get_name_info_request::PendingCacheKey::init(&cache_name); - let cache = resolver.get_or_put_into_resolve_pending_cache::( + let key = PendingCacheKey::::init(&cache_name); + let cache = resolver.get_or_put_into_pending_cache::( &key, PendingCacheField::PendingNameinfoCacheCares, ); diff --git a/src/runtime/dns_jsc/mod.rs b/src/runtime/dns_jsc/mod.rs index a843cb49fbb5..4c0ea475f2c5 100644 --- a/src/runtime/dns_jsc/mod.rs +++ b/src/runtime/dns_jsc/mod.rs @@ -25,11 +25,8 @@ pub mod options_jsc; // GetAddrInfo.Options ↔ JSValue // `Resolver`, and `dispatch.rs`'s `from_field_ptr!`/`owner_as!` casts now resolve // to the same allocation `dns_body::Resolver::init` produces. +pub use dns_body::get_addr_info_request; pub use dns_body::{ CacheConfig, CacheHit, GetAddrInfoAsyncCallback, GetAddrInfoRequest, GlobalData, InternalDNSRequest, Order, PendingCache, PendingCacheField, RecordType, Resolver, internal, }; -pub use dns_body::{ - get_addr_info_request, get_host_by_addr_info_request, get_name_info_request, - resolve_info_request, -}; diff --git a/src/runtime/ffi/host_fns.rs b/src/runtime/ffi/host_fns.rs deleted file mode 100644 index 01e5142af374..000000000000 --- a/src/runtime/ffi/host_fns.rs +++ /dev/null @@ -1,504 +0,0 @@ -//! Bodies for `FFI::{open, close}` and `Function::{compile, -//! print_source_code, print_callback_source_code}` plus the -//! `generate_symbols` / `generate_symbol_for_function` helpers. -//! -//! The JSC-dependent paths are wired against the type identities declared in -//! `super` (`FFI`, `Function`, `ABIType`, `Step`, `Compiled`). -//! -//! TinyCC compile/relocate (`bun_tcc_sys::State` method-ful API) remains -//! gated; `Function::compile` therefore short-circuits with a `Step::Failed` -//! when the `tinycc` feature is off (which it always is until -//! `bun_tcc_sys::tcc` un-gates). The full TCC body is preserved in -//! `ffi_body.rs` (``) for reference. - -use std::ffi::c_void; -use std::io::Write as _; - -use bstr::BStr; - -use bun_collections::StringArrayHashMap; -use bun_core::{self, ZigString}; -use bun_jsc::{self as jsc, JSGlobalObject, JSPropertyIterator, JSValue, JsResult}; - -use super::{ABIType, Function}; - -unsafe extern "C" { - /// `JSValue::getOwn` — own-property lookup (no prototype-chain walk). - /// Declared locally while `bun_jsc::JSValue::get_own` (JSValue.rs) is gated. - fn JSC__JSValue__getOwn( - value: JSValue, - global: *const JSGlobalObject, - name: *const bun_core::String, - ) -> JSValue; -} - -/// Own-property lookup. Local thin -/// wrapper while `bun_jsc::JSValue::get_own` stays gated. -#[inline] -fn get_own(value: JSValue, global: &JSGlobalObject, key: &[u8]) -> JsResult> { - let key_str = bun_core::String::init(ZigString::init(key)); - // Open a top exception scope before the FFI call (the C++ side has a - // ThrowScope whose dtor sets `m_needExceptionCheck`); a post-hoc `has_exception()` - // would assert under `BUN_JSC_validateExceptionChecks=1`. - bun_jsc::top_scope!(scope, global); - // SAFETY: `global` is live; `key_str` borrows `key` for the call duration. - let v = unsafe { JSC__JSValue__getOwn(value, global, &raw const key_str) }; - scope.return_if_exception()?; - if v.is_empty() { Ok(None) } else { Ok(Some(v)) } -} - -// ══════════════════════════════════════════════════════════════════════════ -// Symbol-spec parsing — generate_symbols / generate_symbol_for_function -// ══════════════════════════════════════════════════════════════════════════ - -/// Parse one -/// `{ args, returns, threadsafe, ptr }` spec into a `Function`. -pub fn generate_symbol_for_function( - global: &JSGlobalObject, - value: JSValue, - function: &mut Function, -) -> JsResult> { - jsc::mark_binding!(); - - let mut abi_types: Vec = Vec::new(); - - if let Some(args) = get_own(value, global, b"args")? { - if args.is_empty_or_undefined_or_null() || !args.js_type().is_array() { - return Ok(Some(global.create_error_instance(format_args!( - "Expected an object with \"args\" as an array" - )))); - } - - let mut array = args.array_iterator(global)?; - abi_types.reserve_exact(array.len as usize); - while let Some(val) = array.next()? { - if val.is_empty_or_undefined_or_null() { - return Ok(Some(global.create_error_instance(format_args!( - "param must be a string (type name) or number" - )))); - } - - if val.is_any_int() { - let int = val.to_int32(); - // Reject Buffer (20); only the string-label path accepts it. - if let Some(t) = ABIType::from_int(int).filter(|_| int <= ABIType::MAX) { - abi_types.push(t); - continue; - } else { - return Ok(Some( - global.create_error_instance(format_args!("invalid ABI type")), - )); - } - } - - if !val.js_type().is_string_like() { - return Ok(Some(global.create_error_instance(format_args!( - "param must be a string (type name) or number" - )))); - } - - let type_name = val.to_slice(global)?; - let Some(abi) = ABIType::LABEL.get(type_name.slice()).copied() else { - return Ok(Some(global.create_type_error_instance(format_args!( - "Unknown type {}", - BStr::new(type_name.slice()) - )))); - }; - abi_types.push(abi); - } - } - - let mut return_type = ABIType::Void; - let mut threadsafe = false; - - if let Some(threadsafe_value) = value.get_truthy(global, b"threadsafe")? { - threadsafe = threadsafe_value.to_boolean(); - } - - 'brk: { - if let Some(ret_value) = value.get_truthy(global, b"returns")? { - if ret_value.is_any_int() { - let int = ret_value.to_int32(); - // Reject Buffer (20); only the string-label path accepts it. - if let Some(t) = ABIType::from_int(int).filter(|_| int <= ABIType::MAX) { - return_type = t; - break 'brk; - } else { - return Ok(Some( - global.create_error_instance(format_args!("invalid ABI type")), - )); - } - } - - let ret_slice = ret_value.to_slice(global)?; - return_type = match ABIType::LABEL.get(ret_slice.slice()).copied() { - Some(t) => t, - None => { - return Ok(Some(global.create_type_error_instance(format_args!( - "Unknown return type {}", - BStr::new(ret_slice.slice()) - )))); - } - }; - } - } - - if return_type == ABIType::NapiEnv { - return Ok(Some(global.create_error_instance(format_args!( - "Cannot return napi_env to JavaScript" - )))); - } - - if return_type == ABIType::Buffer { - return Ok(Some(global.create_error_instance(format_args!( - "Cannot return a buffer to JavaScript (since byteLength and byteOffset are unknown)" - )))); - } - - if function.threadsafe && return_type != ABIType::Void { - return Ok(Some(global.create_error_instance(format_args!( - "Threadsafe functions must return void" - )))); - } - - // `Function` has a `Drop` impl, so functional-record-update - // (`..Default::default()`) is rejected (E0509). Reset to default and assign - // the parsed fields individually instead. - *function = Function::default(); - function.arg_types = abi_types; - function.return_type = return_type; - function.threadsafe = threadsafe; - - if let Some(ptr) = value.get(global, b"ptr")? { - if ptr.is_number() { - let num = ptr.as_ptr_address(); - if num > 0 { - function.symbol_from_dynamic_library = Some(num as *mut c_void); - } - } else if ptr.is_heap_big_int() { - let num = ptr.to_uint64_no_truncate() as usize; - if num > 0 { - function.symbol_from_dynamic_library = Some(num as *mut c_void); - } - } - } - - Ok(None) -} - -/// Iterate own-properties of `object`, -/// parsing each value as a `Function` spec. -pub fn generate_symbols( - global: &JSGlobalObject, - symbols: &mut StringArrayHashMap, - object: impl jsc::IntoIterObject, -) -> JsResult> { - jsc::mark_binding!(); - - // skip_empty_name = true, include_value = true, own_only = true - let mut symbols_iter = JSPropertyIterator::init( - global, - object, - jsc::JSPropertyIteratorOptions { - skip_empty_name: true, - include_value: true, - own_properties_only: true, - ..Default::default() - }, - )?; - - symbols.reserve(symbols_iter.len); - - while let Some(prop) = symbols_iter.next()? { - let value = symbols_iter.value; - - if value.is_empty_or_undefined_or_null() || !value.is_object() { - return Ok(Some(global.create_type_error_instance(format_args!( - "Expected an object for key \"{}\"", - prop - )))); - } - - let mut function = Function::default(); - if let Some(val) = generate_symbol_for_function(global, value, &mut function)? { - return Ok(Some(val)); - } - let base_name = prop.to_owned_slice_z(); - let key = base_name.as_bytes().to_vec().into_boxed_slice(); - function.base_name = Some(base_name); - - symbols.insert(&key, function); - } - - Ok(None) -} - -// ══════════════════════════════════════════════════════════════════════════ -// Function — compile + C-source emission -// ══════════════════════════════════════════════════════════════════════════ - -impl Function { - /// Emit the C trampoline that - /// adapts a JSC host-call frame to the native symbol's ABI. - pub fn print_source_code( - &self, - writer: &mut impl std::io::Write, - ) -> Result<(), bun_core::Error> { - if !self.arg_types.is_empty() { - writer.write_all(b"#define HAS_ARGUMENTS\n")?; - } - - 'brk: { - if self.return_type.is_floating_point() { - writer.write_all(b"#define USES_FLOAT 1\n")?; - break 'brk; - } - for arg in self.arg_types.iter() { - // conditionally include math.h - if arg.is_floating_point() { - writer.write_all(b"#define USES_FLOAT 1\n")?; - break; - } - } - } - - writer.write_all(Self::ffi_header())?; - - // -- Generate the FFI function symbol - writer.write_all(b"/* --- The Function To Call */\n")?; - self.return_type.typename(writer)?; - writer.write_all(b" ")?; - writer.write_all(self.base_name.as_ref().map(|b| b.as_bytes()).unwrap_or(b""))?; - writer.write_all(b"(")?; - let mut first = true; - for (i, arg) in self.arg_types.iter().enumerate() { - if !first { - writer.write_all(b", ")?; - } - first = false; - arg.param_typename(writer)?; - write!(writer, " arg{}", i)?; - } - writer.write_all( - b");\n\ - \n\ - /* ---- Your Wrapper Function ---- */\n\ - ZIG_REPR_TYPE JSFunctionCall(void* JS_GLOBAL_OBJECT, void* callFrame) {\n", - )?; - - if self.needs_handle_scope() { - writer.write_all( - b" void* handleScope = NapiHandleScope__open(&Bun__thisFFIModuleNapiEnv, false);\n", - )?; - } - - if !self.arg_types.is_empty() { - writer.write_all(b" LOAD_ARGUMENTS_FROM_CALL_FRAME;\n")?; - for (i, arg) in self.arg_types.iter().enumerate() { - if *arg == ABIType::NapiEnv { - write!( - writer, - " napi_env arg{} = (napi_env)&Bun__thisFFIModuleNapiEnv;\n argsPtr++;\n", - i - )?; - } else if *arg == ABIType::NapiValue { - writeln!( - writer, - " EncodedJSValue arg{} = {{ .asInt64 = *argsPtr++ }};", - i - )?; - } else if arg.needs_a_cast_in_c() { - if i < self.arg_types.len() - 1 { - writeln!( - writer, - " EncodedJSValue arg{} = {{ .asInt64 = *argsPtr++ }};", - i - )?; - } else { - write!( - writer, - " EncodedJSValue arg{};\n arg{}.asInt64 = *argsPtr;\n", - i, i - )?; - } - } else if i < self.arg_types.len() - 1 { - writeln!(writer, " int64_t arg{} = *argsPtr++;", i)?; - } else { - writeln!(writer, " int64_t arg{} = *argsPtr;", i)?; - } - } - } - - let mut arg_buf = [0u8; 32]; - - writer.write_all(b" ")?; - if self.return_type != ABIType::Void { - self.return_type.typename(writer)?; - writer.write_all(b" return_value = ")?; - } - write!( - writer, - "{}(", - BStr::new(self.base_name.as_ref().map(|b| b.as_bytes()).unwrap_or(b"")) - )?; - first = true; - arg_buf[0..3].copy_from_slice(b"arg"); - for (i, arg) in self.arg_types.iter().enumerate() { - if !first { - writer.write_all(b", ")?; - } - first = false; - writer.write_all(b" ")?; - - let length_buf = bun_core::fmt::print_int(&mut arg_buf[3..], i); - let arg_name = &arg_buf[0..3 + length_buf]; - if arg.needs_a_cast_in_c() { - write!(writer, "{}", arg.to_c(arg_name))?; - } else { - writer.write_all(arg_name)?; - } - } - writer.write_all(b");\n")?; - - if !first { - writer.write_all(b"\n")?; - } - - writer.write_all(b" ")?; - - if self.needs_handle_scope() { - writer.write_all( - b" NapiHandleScope__close(&Bun__thisFFIModuleNapiEnv, handleScope);\n", - )?; - } - - writer.write_all(b"return ")?; - - if self.return_type != ABIType::Void { - write!( - writer, - "{}.asZigRepr", - self.return_type.to_js(b"return_value") - )?; - } else { - writer.write_all(b"ValueUndefined.asZigRepr")?; - } - - writer.write_all(b";\n}\n\n")?; - Ok(()) - } - - /// Emit the C - /// trampoline that adapts a native call into a JSC `FFI_Callback_call`. - pub fn print_callback_source_code( - &self, - global_object: Option<&JSGlobalObject>, - context_ptr: Option<*mut c_void>, - writer: &mut impl std::io::Write, - ) -> Result<(), bun_core::Error> { - { - let ptr = global_object - .map(|g| std::ptr::from_ref(g) as usize) - .unwrap_or(0); - writeln!(writer, "#define JS_GLOBAL_OBJECT (void*)0x{:X}ULL", ptr)?; - } - - writer.write_all(b"#define IS_CALLBACK 1\n")?; - - 'brk: { - if self.return_type.is_floating_point() { - writer.write_all(b"#define USES_FLOAT 1\n")?; - break 'brk; - } - for arg in self.arg_types.iter() { - if arg.is_floating_point() { - writer.write_all(b"#define USES_FLOAT 1\n")?; - break; - } - } - } - - writer.write_all(Self::ffi_header())?; - - // -- Generate the FFI function symbol - writer.write_all(b"\n \n/* --- The Callback Function */\n")?; - let mut first = true; - self.return_type.typename(writer)?; - - writer.write_all(b" my_callback_function")?; - writer.write_all(b"(")?; - for (i, arg) in self.arg_types.iter().enumerate() { - if !first { - writer.write_all(b", ")?; - } - first = false; - arg.typename(writer)?; - write!(writer, " arg{}", i)?; - } - writer.write_all(b") {\n")?; - - if cfg!(debug_assertions) { - writer.write_all(b"#ifdef INJECT_BEFORE\n")?; - writer.write_all(b"INJECT_BEFORE;\n")?; - writer.write_all(b"#endif\n")?; - } - - first = true; - let _ = first; - - if !self.arg_types.is_empty() { - let mut arg_buf = [0u8; 32]; - writeln!( - writer, - " ZIG_REPR_TYPE arguments[{}];", - self.arg_types.len() - )?; - - arg_buf[0..3].copy_from_slice(b"arg"); - for (i, arg) in self.arg_types.iter().enumerate() { - let printed = bun_core::fmt::print_int(&mut arg_buf[3..], i); - let arg_name = &arg_buf[0..3 + printed]; - writeln!( - writer, - "arguments[{}] = {}.asZigRepr;", - i, - arg.to_js(arg_name) - )?; - } - } - - writer.write_all(b" ")?; - let mut inner_buf_ = [0u8; 372]; - - let written = { - let ptr = context_ptr.map(|p| p as usize).unwrap_or(0); - let mut cursor = std::io::Cursor::new(&mut inner_buf_[1..]); - if !self.arg_types.is_empty() { - write!( - &mut cursor, - "FFI_Callback_call((void*)0x{:X}ULL, {}, arguments)", - ptr, - self.arg_types.len() - )?; - } else { - write!( - &mut cursor, - "FFI_Callback_call((void*)0x{:X}ULL, 0, (ZIG_REPR_TYPE*)0)", - ptr - )?; - } - cursor.position() as usize - }; - - if self.return_type == ABIType::Void { - writer.write_all(&inner_buf_[1..1 + written])?; - } else { - inner_buf_[0] = b'_'; - let inner_buf = &inner_buf_[0..1 + written]; - write!(writer, "return {}", self.return_type.to_c_exact(inner_buf))?; - } - - writer.write_all(b";\n}\n\n")?; - Ok(()) - } -} diff --git a/src/runtime/ffi/mod.rs b/src/runtime/ffi/mod.rs index 1e5800bf823c..debf00cc75ed 100644 --- a/src/runtime/ffi/mod.rs +++ b/src/runtime/ffi/mod.rs @@ -1,21 +1,11 @@ //! `Bun.FFI` / `bun:ffi`. //! -//! `ABIType` (CType) enum, `FFI`/`Function`/`Step`/`Compiled` structs, -//! formatters, dlopen data path, and the JSC host-fn entry points -//! (`open`/`close`/`compile`/`generate_symbols`) are real. The full TinyCC -//! compile bodies (`CompileC`, `Function::compile`, `cc`/`linkSymbols`/ -//! `callback`) live in `ffi_body` on top of `bun_tcc_sys::State`. - -use core::ffi::{c_char, c_void}; -use core::ptr::NonNull; - -use bun_core::ZBox; - -use crate::jsc::JSGlobalObject; - -// ─── un-gated host-fn bodies (open/close/compile/generate_symbols) ─────────── -mod host_fns; -pub use host_fns::{generate_symbol_for_function, generate_symbols}; +//! `ABIType` (CType) enum and formatters live in `abi_type`; everything else — +//! `FFI`/`Function`/`Step`/`Compiled` structs, dlopen data path, the JSC +//! host-fn entry points (`open`/`close`/`compile`/`generate_symbols`), and the +//! full TinyCC compile bodies (`CompileC`, `Function::compile`, +//! `cc`/`linkSymbols`/`callback`) — lives in `ffi_body` on top of +//! `bun_tcc_sys::State`. // ─── implementation modules ────────────────────────────────────────────────── @@ -104,11 +94,6 @@ mod dom_call_slowpath { } } -// `bun_tcc_sys` provides the method-ful `State` (compile_string/relocate/ -// add_symbol/…) with per-target link stubs where TinyCC isn't built — see -// `tcc_externs!` in `src/tcc_sys/tcc.rs`. -use bun_tcc_sys as TCC; - /// Get the last dynamic-library loading error message in a cross-platform way. /// On POSIX systems, this calls `dlerror()`. /// On Windows, this uses `GetLastError()` and formats the error code. @@ -150,124 +135,6 @@ pub(crate) fn get_dl_error() -> Box<[u8]> { pub use ffi_body::FFI; -// The full `CompileC`/`Source`/`SymbolsMap`/`StringArray`/`CompilerRT` port -// lives in `ffi_body`; the draft duplicates that used to sit here were unused -// and have been removed. - -// ─── Function ──────────────────────────────────────────────────────────────── - -pub struct Function { - pub symbol_from_dynamic_library: Option<*mut c_void>, - pub base_name: Option, - pub state: Option>, - - pub return_type: ABIType, - pub arg_types: Vec, - pub step: Step, - pub threadsafe: bool, - // allocator field dropped — global mimalloc -} - -impl Default for Function { - fn default() -> Self { - Self { - symbol_from_dynamic_library: None, - base_name: None, - state: None, - return_type: ABIType::Void, - arg_types: Vec::new(), - step: Step::Pending, - threadsafe: false, - } - } -} - -// PORTING.md §Global mutable state: written once at startup with the -// resolved tinycc lib dir; read by the FFI compile path. RacyCell over the -// raw C-string pointer (no concurrent writers). -pub static LIB_DIR_Z: bun_core::RacyCell<*const c_char> = bun_core::RacyCell::new(c"".as_ptr()); - -unsafe extern "C" { - fn FFICallbackFunctionWrapper_destroy(_: *mut c_void); -} - -impl Drop for Function { - fn drop(&mut self) { - // base_name, arg_types, Step::Failed.msg are owned and freed by drop glue. - if let Some(state) = self.state.take() { - // SAFETY: `state` is the live TCCState* allocated for this Function's - // trampoline; ownership is unique here (taken from self). - unsafe { TCC::State::destroy(state.as_ptr()) }; - } - if let Step::Compiled(compiled) = &mut self.step { - if let Some(wrapper) = compiled.ffi_callback_function_wrapper.take() { - // SAFETY: wrapper was created by Bun__createFFICallbackFunction - unsafe { FFICallbackFunctionWrapper_destroy(wrapper.as_ptr()) }; - } - } - } -} - -impl Function { - pub fn needs_handle_scope(&self) -> bool { - for arg in self.arg_types.iter() { - if *arg == ABIType::NapiEnv || *arg == ABIType::NapiValue { - return true; - } - } - self.return_type == ABIType::NapiValue - } - - pub fn needs_napi_env(&self) -> bool { - for arg in self.arg_types.iter() { - if *arg == ABIType::NapiEnv || *arg == ABIType::NapiValue { - return true; - } - } - false - } - - pub fn ffi_header() -> &'static [u8] { - // Embedded under - // `codegen_embed`, reloaded from disk otherwise (dev fast iteration). - bun_core::runtime_embed_file!(Src, "runtime/ffi/FFI.h").as_bytes() - } -} - -// ─── Step ──────────────────────────────────────────────────────────────────── - -pub enum Step { - Pending, - Compiled(Compiled), - Failed { msg: Box<[u8]>, allocated: bool }, -} - -/// Draft-path sibling of `ffi_body::Compiled`; see it for JS function rooting. -pub struct Compiled { - pub ptr: *mut c_void, - pub js_context: Option<*mut JSGlobalObject>, - pub ffi_callback_function_wrapper: Option>, -} - -impl Default for Compiled { - fn default() -> Self { - Self { - ptr: core::ptr::null_mut(), - js_context: None, - ffi_callback_function_wrapper: None, - } - } -} - -impl Step { - pub fn compiled_ptr(&self) -> *mut c_void { - match self { - Step::Compiled(c) => c.ptr, - _ => core::ptr::null_mut(), - } - } -} - // ═════════════════════════════════════════════════════════════════════════════ // ABIType — single source of truth lives in abi_type.rs // ═════════════════════════════════════════════════════════════════════════════ diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index e7eac1d2afab..2e6f4c769f8a 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -843,15 +843,22 @@ unsafe fn ensure_debugger(vm: *mut VirtualMachine, block_until_connected: bool) } } -/// `eventLoop().autoTick()`. Needs -/// `timer::All` for the poll-timeout calculation, hence dispatched here. +/// `eventLoop().autoTick()` (`ACTIVE = false`) and `eventLoop().autoTickActive()` +/// (`ACTIVE = true`). Needs `timer::All` for the poll-timeout calculation, +/// hence dispatched here. +/// +/// The active variant skips `runImminentGCTimer` and the +/// `handleRejectedPromises` tails; it is used by `bun_main` / `on_before_exit` +/// drain loops where blocking when the loop is idle would hang shutdown. +/// `ACTIVE` is const-generic so both variants monomorphize with no runtime +/// branch on this hot path. /// /// PERF: the one fn-ptr indirection is dwarfed by the kqueue/epoll syscall it /// gates. /// /// # Safety /// `vm` is the live per-thread VM. -unsafe fn auto_tick(vm: *mut VirtualMachine) { +unsafe fn auto_tick(vm: *mut VirtualMachine) { // Note: reshaped for borrowck — `EventLoop` is a value field of // `VirtualMachine`, so holding `&mut EventLoop` while also touching VM // siblings would alias. Dereference per-field via the raw `vm` ptr. @@ -903,8 +910,10 @@ unsafe fn auto_tick(vm: *mut VirtualMachine) { .update_date_header_timer_if_necessary(&*loop_, vm) }; } - // SAFETY: `el` is the live per-thread event loop. - unsafe { (*el).run_imminent_gc_timer() }; + if !ACTIVE { + // SAFETY: `el` is the live per-thread event loop. + unsafe { (*el).run_imminent_gc_timer() }; + } // ── poll the I/O loop with the next-timer deadline ────────────────── if state.is_null() { @@ -917,8 +926,10 @@ unsafe fn auto_tick(vm: *mut VirtualMachine) { // Still run the post-poll hooks. // SAFETY: per fn contract. unsafe { (*vm).on_after_event_loop() }; - // SAFETY: `vm.global` is set during `VirtualMachine::init` and outlives the VM. - unsafe { (*(*vm).global).handle_rejected_promises() }; + if !ACTIVE { + // SAFETY: `vm.global` is set during `VirtualMachine::init` and outlives the VM. + unsafe { (*(*vm).global).handle_rejected_promises() }; + } return; } @@ -993,115 +1004,10 @@ unsafe fn auto_tick(vm: *mut VirtualMachine) { // SAFETY: per fn contract. unsafe { (*vm).on_after_event_loop() }; - // SAFETY: `vm.global` is set during `VirtualMachine::init` and outlives the VM. - unsafe { (*(*vm).global).handle_rejected_promises() }; -} - -/// `eventLoop().autoTickActive()`. Same shape as -/// [`auto_tick`] but: no `runImminentGCTimer`, no `handleRejectedPromises` at -/// the tail, and no debug sleep-timer logging. Used by `bun_main` / -/// `on_before_exit` drain loops where blocking when the loop is idle would -/// hang shutdown. -/// -/// # Safety -/// `vm` is the live per-thread VM. -unsafe fn auto_tick_active(vm: *mut VirtualMachine) { - // Note: reshaped for borrowck — see `auto_tick` above. - // SAFETY: per fn contract — `vm` is the live per-thread VM. - let el: *mut bun_jsc::event_loop::EventLoop = unsafe { &*vm }.event_loop; - // SAFETY: `el` is the live per-thread event loop (field of `*vm`). - let loop_ = unsafe { (*el).usockets_loop() }; - - // SAFETY: `el` is the live per-thread event loop; `vm` per fn contract. - unsafe { (*el).tick_immediate_tasks(vm) }; - #[cfg(windows)] - if !unsafe { &*el }.immediate_tasks.is_empty() { - // SAFETY: `el` is the live per-thread event loop. - unsafe { (*el).wakeup() }; - } - - #[cfg(unix)] - { - // SAFETY: per fn contract. `swap(0)` so a concurrent - // `increment_pending_unref_counter()` (cross-thread, see - // `KeepAlive::unref_on_next_tick_concurrently`) can't be lost between - // the read and the reset. - let pending_unref = unsafe { &*vm } - .pending_unref_counter - .swap(0, core::sync::atomic::Ordering::Relaxed); - if pending_unref > 0 { - // SAFETY: `loop_` is the live per-thread uws loop. - unsafe { (*loop_).unref_count(pending_unref) }; - } - } - - let state = runtime_state(); - if !state.is_null() { - // SAFETY: see the matching call in `auto_tick` above. - unsafe { - (*state) - .timer - .update_date_header_timer_if_necessary(&*loop_, vm) - }; - } - - if state.is_null() { - // SAFETY: `loop_` is the live per-thread uws loop. - unsafe { (*loop_).tick_without_idle() }; - // SAFETY: per fn contract. - unsafe { (*vm).on_after_event_loop() }; - return; - } - - { - // SAFETY: `el` is the live per-thread event loop. - let has_pending_immediate = !unsafe { &*el }.immediate_tasks.is_empty(); - // SAFETY: `loop_` is the live per-thread uws loop. - let quic_next_tick_us = unsafe { - let ild = &(*loop_).internal_loop_data; - if ild.quic_head.is_null() { - None - } else { - Some(ild.quic_next_tick_us) - } - }; - let mut timespec = bun_core::Timespec { sec: 0, nsec: 0 }; - // SAFETY: `loop_` is the live per-thread uws loop. - if unsafe { (*loop_).is_active() } { - // SAFETY: `el` is the live per-thread event loop. - unsafe { (*el).process_gc_timer() }; - // SAFETY: `state` is the live per-thread `RuntimeState`; see - // Note on `auto_tick` re: aliased-&mut across `fire()`. - let have_timeout = unsafe { - timer::All::get_timeout( - &mut (*state).timer, - &mut timespec, - has_pending_immediate, - quic_next_tick_us, - vm.cast(), - ) - }; - // SAFETY: `loop_` is the live per-thread uws loop. - unsafe { - (*loop_).tick_with_timeout(if have_timeout { Some(×pec) } else { None }) - }; - } else { - // SAFETY: `loop_` is the live per-thread uws loop. - unsafe { (*loop_).tick_without_idle() }; - } - } - - #[cfg(unix)] - { - // SAFETY: `state` is the live per-thread `RuntimeState`; see Note - // on `auto_tick` re: aliased-&mut across `fire()`. - unsafe { timer::All::drain_timers(&mut (*state).timer, vm.cast()) }; + if !ACTIVE { + // SAFETY: `vm.global` is set during `VirtualMachine::init` and outlives the VM. + unsafe { (*(*vm).global).handle_rejected_promises() }; } - #[cfg(not(unix))] - let _ = state; - - // SAFETY: per fn contract. - unsafe { (*vm).on_after_event_loop() }; } /// `printException` / `printErrorlikeObject` — formats `value` to stderr via @@ -1452,8 +1358,8 @@ pub(crate) static __BUN_RUNTIME_HOOKS: RuntimeHooks = RuntimeHooks { generate_entry_point, load_preloads, ensure_debugger, - auto_tick, - auto_tick_active, + auto_tick: auto_tick::, + auto_tick_active: auto_tick::, print_exception, timer_insert, timer_remove, diff --git a/src/runtime/napi/napi_body.rs b/src/runtime/napi/napi_body.rs index 600d9dcfac17..4a58d2aad5d3 100644 --- a/src/runtime/napi/napi_body.rs +++ b/src/runtime/napi/napi_body.rs @@ -641,6 +641,78 @@ pub(super) extern "C" fn napi_create_int64( env.ok() } +/// Code-unit type accepted by the `napi_create_string_*` entry points; selects +/// how a `NAPI_AUTO_LENGTH` (NUL-terminated) input is measured. +trait NapiStringUnit: Copy { + /// # Safety + /// `ptr` must be non-null and point to a NUL-terminated sequence. + unsafe fn cstr_units<'a>(ptr: *const Self) -> &'a [Self]; +} + +impl NapiStringUnit for u8 { + #[inline(always)] + unsafe fn cstr_units<'a>(ptr: *const u8) -> &'a [u8] { + // SAFETY: forwarded caller contract. + unsafe { bun_core::ffi::cstr(ptr.cast::()) }.to_bytes() + } +} + +impl NapiStringUnit for u16 { + #[inline(always)] + unsafe fn cstr_units<'a>(ptr: *const u16) -> &'a [u16] { + // SAFETY: forwarded caller contract. Scans to the NUL u16 terminator. + unsafe { bun_core::ffi::wstr_units(ptr) } + } +} + +/// Shared argument-validation prologue for the `napi_create_string_*` entry +/// points: extracts the source code units, or `Err(())` when the arguments +/// are invalid (caller returns `env.invalid_arg()`). +/// +/// # Safety +/// When `str_` is non-null, the NAPI caller contract must hold: if `length == +/// NAPI_AUTO_LENGTH`, `str_` points to a NUL-terminated sequence; otherwise +/// `[str_, str_ + length)` must be readable. The returned borrow has an +/// unconstrained lifetime and must not outlive the caller's buffer. +#[inline(always)] +unsafe fn napi_string_slice<'a, T: NapiStringUnit>( + str_: *const T, + length: usize, +) -> Result<&'a [T], ()> { + if !str_.is_null() { + if NAPI_AUTO_LENGTH == length { + // SAFETY: caller guarantees ptr is NUL-terminated when length == NAPI_AUTO_LENGTH. + Ok(unsafe { T::cstr_units(str_) }) + } else if length > i32::MAX as usize { + Err(()) + } else { + // SAFETY: caller guarantees [ptr, ptr+length) is valid. + Ok(unsafe { bun_core::ffi::slice(str_, length) }) + } + } else if length == 0 { + Ok(&[]) + } else { + Err(()) + } +} + +/// Writes a converted string's `to_js` result through the out-param, mapping +/// conversion failure to `generic_failure`. +#[inline(always)] +fn set_string_result( + env: &NapiEnv, + result: &mut napi_value, + js: jsc::JsResult, +) -> napi_status { + match js { + Ok(v) => { + result.set(env, v); + env.ok() + } + Err(_) => NapiEnv::set_last_error(Some(env), NapiStatus::generic_failure), + } +} + #[unsafe(no_mangle)] pub(super) extern "C" fn napi_create_string_latin1( env_: napi_env, @@ -651,24 +723,11 @@ pub(super) extern "C" fn napi_create_string_latin1( let env = get_env!(env_); let result = get_out!(env, result_); - let slice: &[u8] = 'brk: { - if !str_.is_null() { - if NAPI_AUTO_LENGTH == length { - // SAFETY: caller guarantees ptr is NUL-terminated when length == NAPI_AUTO_LENGTH. - break 'brk unsafe { bun_core::ffi::cstr(str_.cast::()) }.to_bytes(); - } else if length > i32::MAX as usize { - return env.invalid_arg(); - } else { - // SAFETY: caller guarantees [ptr, ptr+length) is valid. - break 'brk unsafe { bun_core::ffi::slice(str_, length) }; - } - } - - if length == 0 { - break 'brk &[]; - } else { - return env.invalid_arg(); - } + // SAFETY: NAPI caller contract — `str_` is NUL-terminated when `length == + // NAPI_AUTO_LENGTH`, otherwise `[str_, str_ + length)` is readable; the + // slice is consumed before this call returns. + let Ok(slice) = (unsafe { napi_string_slice(str_, length) }) else { + return env.invalid_arg(); }; bun_output::scoped_log!( @@ -678,24 +737,14 @@ pub(super) extern "C" fn napi_create_string_latin1( ); if slice.is_empty() { - let js = match bun_core::String::empty().to_js(env.to_js()) { - Ok(v) => v, - Err(_) => return NapiEnv::set_last_error(Some(env), NapiStatus::generic_failure), - }; - result.set(env, js); - return env.ok(); + return set_string_result(env, result, bun_core::String::empty().to_js(env.to_js())); } let (string, bytes) = bun_core::String::create_uninitialized_latin1(slice.len()); // `string` derefs on Drop. bytes.copy_from_slice(slice); - let js = match string.to_js(env.to_js()) { - Ok(v) => v, - Err(_) => return NapiEnv::set_last_error(Some(env), NapiStatus::generic_failure), - }; - result.set(env, js); - env.ok() + set_string_result(env, result, string.to_js(env.to_js())) } #[unsafe(no_mangle)] @@ -708,24 +757,11 @@ pub(super) extern "C" fn napi_create_string_utf8( let env = get_env!(env_); let result = get_out!(env, result_); - let slice: &[u8] = 'brk: { - if !str_.is_null() { - if NAPI_AUTO_LENGTH == length { - // SAFETY: caller guarantees ptr is NUL-terminated when length == NAPI_AUTO_LENGTH. - break 'brk unsafe { bun_core::ffi::cstr(str_.cast::()) }.to_bytes(); - } else if length > i32::MAX as usize { - return env.invalid_arg(); - } else { - // SAFETY: caller guarantees [ptr, ptr+length) is valid. - break 'brk unsafe { bun_core::ffi::slice(str_, length) }; - } - } - - if length == 0 { - break 'brk &[]; - } else { - return env.invalid_arg(); - } + // SAFETY: NAPI caller contract — `str_` is NUL-terminated when `length == + // NAPI_AUTO_LENGTH`, otherwise `[str_, str_ + length)` is readable; the + // slice is consumed before this call returns. + let Ok(slice) = (unsafe { napi_string_slice(str_, length) }) else { + return env.invalid_arg(); }; bun_output::scoped_log!(napi, "napi_create_string_utf8: {}", bstr::BStr::new(slice)); @@ -749,25 +785,11 @@ pub(super) extern "C" fn napi_create_string_utf16( let env = get_env!(env_); let result = get_out!(env, result_); - let slice: &[u16] = 'brk: { - if !str_.is_null() { - if NAPI_AUTO_LENGTH == length { - // SAFETY: caller guarantees ptr is NUL-terminated when length == NAPI_AUTO_LENGTH. - // Scan to the NUL u16 terminator. - break 'brk unsafe { bun_core::ffi::wstr_units(str_) }; - } else if length > i32::MAX as usize { - return env.invalid_arg(); - } else { - // SAFETY: caller guarantees [ptr, ptr+length) is valid. - break 'brk unsafe { bun_core::ffi::slice(str_, length) }; - } - } - - if length == 0 { - break 'brk &[]; - } else { - return env.invalid_arg(); - } + // SAFETY: NAPI caller contract — `str_` is NUL-terminated when `length == + // NAPI_AUTO_LENGTH`, otherwise `[str_, str_ + length)` is readable; the + // slice is consumed before this call returns. + let Ok(slice) = (unsafe { napi_string_slice(str_, length) }) else { + return env.invalid_arg(); }; if cfg!(debug_assertions) { @@ -780,23 +802,13 @@ pub(super) extern "C" fn napi_create_string_utf16( } if slice.is_empty() { - let js = match bun_core::String::empty().to_js(env.to_js()) { - Ok(v) => v, - Err(_) => return NapiEnv::set_last_error(Some(env), NapiStatus::generic_failure), - }; - result.set(env, js); - return env.ok(); + return set_string_result(env, result, bun_core::String::empty().to_js(env.to_js())); } let (mut string, chars) = bun_core::String::create_uninitialized_utf16(slice.len()); chars.copy_from_slice(slice); - let js = match string.transfer_to_js(env.to_js()) { - Ok(v) => v, - Err(_) => return NapiEnv::set_last_error(Some(env), NapiStatus::generic_failure), - }; - result.set(env, js); - env.ok() + set_string_result(env, result, string.transfer_to_js(env.to_js())) } // Implemented in C++ (napi.cpp); declared extern here for Rust-side callers. diff --git a/src/runtime/node/node_fs.rs b/src/runtime/node/node_fs.rs index 09caf024833b..261e56aaa515 100644 --- a/src/runtime/node/node_fs.rs +++ b/src/runtime/node/node_fs.rs @@ -2909,32 +2909,8 @@ pub mod args { // `Drop for PathLike` covers every // error return below (including `validate_integer`). let path = PathLike::from_js_required(ctx, arguments, "path")?; - let uid: UidT = 'brk: { - let Some(uid_value) = arguments.next() else { - return Err(ctx.throw_invalid_arguments(format_args!("uid is required"))); - }; - arguments.eat(); - break 'brk wrap_to::(validators::validate_integer( - ctx, - uid_value, - "uid", - Some(-1), - Some(u32::MAX as i64), - )?); - }; - let gid: GidT = 'brk: { - let Some(gid_value) = arguments.next() else { - return Err(ctx.throw_invalid_arguments(format_args!("gid is required"))); - }; - arguments.eat(); - break 'brk wrap_to::(validators::validate_integer( - ctx, - gid_value, - "gid", - Some(-1), - Some(u32::MAX as i64), - )?); - }; + let uid = id_from_js(ctx, arguments, "uid")?; + let gid = id_from_js(ctx, arguments, "gid")?; Ok(Chown { path, uid, gid }) } } @@ -2948,32 +2924,8 @@ pub mod args { pub fn to_thread_safe(&self) {} pub fn from_js(ctx: &JSGlobalObject, arguments: &mut ArgumentsSlice) -> JsResult { let fd = FD::from_js_required(ctx, arguments)?; - let uid: UidT = 'brk: { - let Some(uid_value) = arguments.next() else { - return Err(ctx.throw_invalid_arguments(format_args!("uid is required"))); - }; - arguments.eat(); - break 'brk wrap_to::(validators::validate_integer( - ctx, - uid_value, - "uid", - Some(-1), - Some(u32::MAX as i64), - )?); - }; - let gid: GidT = 'brk: { - let Some(gid_value) = arguments.next() else { - return Err(ctx.throw_invalid_arguments(format_args!("gid is required"))); - }; - arguments.eat(); - break 'brk wrap_to::(validators::validate_integer( - ctx, - gid_value, - "gid", - Some(-1), - Some(u32::MAX as i64), - )?); - }; + let uid = id_from_js(ctx, arguments, "uid")?; + let gid = id_from_js(ctx, arguments, "gid")?; Ok(Fchown { fd, uid, gid }) } } @@ -2992,8 +2944,58 @@ pub mod args { T::from(in_ as u8) } + /// Reads a required `uid`/`gid` argument, validated to `[-1, u32::MAX]`. + /// `uid_t` and `gid_t` are the same primitive on every supported platform + /// (`u32` on POSIX, libuv's `unsigned char` on Windows), so one reader + /// serves both. + fn id_from_js( + ctx: &JSGlobalObject, + arguments: &mut ArgumentsSlice, + name: &str, + ) -> JsResult { + let Some(value) = arguments.next() else { + return Err(ctx.throw_invalid_arguments(format_args!("{name} is required"))); + }; + arguments.eat(); + Ok(wrap_to(validators::validate_integer( + ctx, + value, + name, + Some(-1), + Some(u32::MAX as i64), + )?)) + } + pub type LChown = Chown; + fn time_arg_from_js( + ctx: &JSGlobalObject, + arguments: &mut ArgumentsSlice, + name: &str, + ) -> JsResult { + let time = node::time_like_from_js( + ctx, + arguments + .next() + .ok_or_else(|| ctx.throw_invalid_arguments(format_args!("{name} is required")))?, + )? + .ok_or_else(|| { + ctx.throw_invalid_arguments(format_args!("{name} must be a number or a Date")) + })?; + arguments.eat(); + Ok(time) + } + + /// Parse the `atime, mtime` argument pair shared by `utimes`/`lutimes`/`futimes`. + fn times_from_js( + ctx: &JSGlobalObject, + arguments: &mut ArgumentsSlice, + ) -> JsResult<(TimeLike, TimeLike)> { + let atime = time_arg_from_js(ctx, arguments, "atime")?; + let mtime = time_arg_from_js(ctx, arguments, "mtime")?; + Ok((atime, mtime)) + } + pub struct Lutimes { pub path: PathLike, pub atime: TimeLike, @@ -3003,28 +3005,9 @@ pub mod args { impl Lutimes { pub fn from_js(ctx: &JSGlobalObject, arguments: &mut ArgumentsSlice) -> JsResult { // `Drop for PathLike` covers the - // `time_like_from_js` throws below. + // `times_from_js` throws below. let path = PathLike::from_js_required(ctx, arguments, "path")?; - let atime = node::time_like_from_js( - ctx, - arguments.next().ok_or_else(|| { - ctx.throw_invalid_arguments(format_args!("atime is required")) - })?, - )? - .ok_or_else(|| { - ctx.throw_invalid_arguments(format_args!("atime must be a number or a Date")) - })?; - arguments.eat(); - let mtime = node::time_like_from_js( - ctx, - arguments.next().ok_or_else(|| { - ctx.throw_invalid_arguments(format_args!("mtime is required")) - })?, - )? - .ok_or_else(|| { - ctx.throw_invalid_arguments(format_args!("mtime must be a number or a Date")) - })?; - arguments.eat(); + let (atime, mtime) = times_from_js(ctx, arguments)?; Ok(Lutimes { path, atime, mtime }) } } @@ -3666,26 +3649,7 @@ pub mod args { pub fn to_thread_safe(&self) {} pub fn from_js(ctx: &JSGlobalObject, arguments: &mut ArgumentsSlice) -> JsResult { let fd = FD::from_js_required(ctx, arguments)?; - let atime = node::time_like_from_js( - ctx, - arguments.next().ok_or_else(|| { - ctx.throw_invalid_arguments(format_args!("atime is required")) - })?, - )? - .ok_or_else(|| { - ctx.throw_invalid_arguments(format_args!("atime must be a number or a Date")) - })?; - arguments.eat(); - let mtime = node::time_like_from_js( - ctx, - arguments.next().ok_or_else(|| { - ctx.throw_invalid_arguments(format_args!("mtime is required")) - })?, - )? - .ok_or_else(|| { - ctx.throw_invalid_arguments(format_args!("mtime must be a number or a Date")) - })?; - arguments.eat(); + let (atime, mtime) = times_from_js(ctx, arguments)?; Ok(Futimes { fd, atime, mtime }) } } @@ -4808,6 +4772,18 @@ impl Default for NodeFS { // yet in-tree) keep working via `node::fs::ReturnType::Foo`. pub use ret as ReturnType; +/// How `copy_file_range_with_fallbacks` handles EINTR from copy_file_range(2), +/// preserving the historical split between its two callers: `fs.copyFile` +/// retries while `fs.cp` surfaces it (matching the original implementations). +#[cfg(any(target_os = "linux", target_os = "android"))] +#[derive(Clone, Copy, PartialEq, Eq)] +enum EintrPolicy { + /// Retry the syscall (`fs.copyFile`). + Retry, + /// Surface EINTR to the caller as an error (`fs.cp`). + Surface, +} + impl NodeFS { pub fn access(&mut self, args: &args::Access, _: Flavor) -> Maybe { // The `bun_sys::access` Windows @@ -5022,6 +4998,105 @@ impl NodeFS { Ok(()) } + /// Shared Linux copy loop for `fs.copyFile` and `fs.cp` once both fds are + /// open and the ioctl_ficlone fast path has been ruled out: drains + /// `src_fd` into `dest_fd` via copy_file_range(2) (Linux 5.3+; not + /// supported in gVisor), falling back to sendfile/read-write when the + /// syscall is unavailable or the filesystem rejects it. Takes ownership of + /// `dest_fd`: on every exit path it is ftruncated to the bytes written, + /// fchmod'd to `st_mode`, and closed. + #[cfg(any(target_os = "linux", target_os = "android"))] + fn copy_file_range_with_fallbacks( + src: &ZStr, + dest: &ZStr, + src_fd: FD, + dest_fd: FD, + st_mode: Mode, + mut size: usize, + eintr_policy: EintrPolicy, + ) -> Maybe { + // `wrote` is read by the deferred-close scopeguard *after* the copy + // loop below mutates it. `Cell` lets the guard borrow by + // reference while the loop `get`s/`set`s, so the value observed at + // scope-exit time is the final one. + let wrote: core::cell::Cell = core::cell::Cell::new(0); + let _close_dest = scopeguard::guard((dest_fd, st_mode, &wrote), |(fd, m, wrote)| { + // ftruncate/fchmod take only ints — no memory-safety preconditions; + // route through the existing `bun_sys` safe wrappers. + let _ = Syscall::ftruncate(fd, (wrote.get() & ((1u64 << 63) - 1)) as i64); + let _ = Syscall::fchmod(fd, m); + fd.close(); + }); + + let mut off_in_copy: i64 = 0; + let mut off_out_copy: i64 = 0; + + if !sys::copy_file::can_use_copy_file_range_syscall() { + let mut w = wrote.get(); + let r = Self::copy_file_using_sendfile_on_linux_with_read_write_fallback( + src, dest, src_fd, dest_fd, size, &mut w, + ); + wrote.set(w); + return r; + } + + // size == 0 means the source stat'd as empty (e.g. procfs): copy + // page-sized chunks until EOF instead of trusting the stat size. + let until_eof = size == 0; + loop { + let chunk = if until_eof { sys::page_size() } else { size }; + // SAFETY: src_fd/dest_fd are valid open fds; copy_file_range is the libc FFI + let written = unsafe { + sys::linux::copy_file_range( + src_fd.native(), + &raw mut off_in_copy, + dest_fd.native(), + &raw mut off_out_copy, + chunk, + 0, + ) + }; + if let Some(err) = Maybe::::errno_sys_p( + written, + sys::Tag::copy_file_range, + dest.as_bytes(), + ) { + match err.get_errno() { + E::EINTR if eintr_policy == EintrPolicy::Retry => continue, + // EINVAL: eCryptfs and other filesystems may not support copy_file_range + // XDEV: cross-device copy not supported + // NOSYS: syscall not available + // OPNOTSUPP: filesystem doesn't support this operation + E::EXDEV | E::ENOSYS | E::EINVAL | E::EOPNOTSUPP => { + if matches!(err.get_errno(), E::ENOSYS | E::EOPNOTSUPP) { + sys::copy_file::disable_copy_file_range_syscall(); + } + let mut w = wrote.get(); + let r = Self::copy_file_using_sendfile_on_linux_with_read_write_fallback( + src, dest, src_fd, dest_fd, size, &mut w, + ); + wrote.set(w); + return r; + } + _ => return err, + } + } + // wrote zero bytes means EOF + if written == 0 { + break; + } + wrote.set(wrote.get().saturating_add(written as u64)); + if !until_eof { + size = size.saturating_sub(written as usize); + if size == 0 { + break; + } + } + } + + Ok(()) + } + pub fn copy_file(&mut self, args: &args::CopyFile, _: Flavor) -> Maybe { match self.copy_file_inner(args) { Ok(_) => Ok(()), @@ -5274,20 +5349,13 @@ impl NodeFS { } let mut flags: i32 = sys::O::CREAT | sys::O::WRONLY; - // VERIFY-FIX(round1): `wrote` is read by the deferred-close scopeguard - // *after* the copy loops below mutate it. As a `usize` captured by-copy - // the guard always saw 0, and the `&mut (wrote as u64)` call sites - // wrote into discarded temporaries. `Cell` lets the guard borrow - // by reference while the loops `get`/`set`, so the value observed at - // scope-exit time is the final one. - let wrote: core::cell::Cell = core::cell::Cell::new(0); if args.mode.shouldnt_overwrite() { flags |= sys::O::EXCL; } let dest_fd = Syscall::open(dest, flags, DEFAULT_PERMISSION)?; - let mut size: usize = stat_.st_size.max(0) as usize; + let size: usize = stat_.st_size.max(0) as usize; // https://manpages.debian.org/testing/manpages-dev/ioctl_ficlone.2.en.html if args.mode.is_force_clone() { @@ -5319,109 +5387,15 @@ impl NodeFS { sys::copy_file::disable_ioctl_ficlone(); } - let _close_dest = - scopeguard::guard((dest_fd, stat_.st_mode, &wrote), |(fd, m, wrote)| { - // ftruncate/fchmod take only ints — no memory-safety preconditions; route - // through the existing `bun_sys` safe wrappers (same as lines above). - let _ = Syscall::ftruncate(fd, (wrote.get() & ((1u64 << 63) - 1)) as i64); - let _ = Syscall::fchmod(fd, m as u32); - fd.close(); - }); - - let mut off_in_copy: i64 = 0; - let mut off_out_copy: i64 = 0; - - if !sys::copy_file::can_use_copy_file_range_syscall() { - let mut w = wrote.get(); - let r = Self::copy_file_using_sendfile_on_linux_with_read_write_fallback( - src, dest, src_fd, dest_fd, size, &mut w, - ); - wrote.set(w); - return r; - } - - if size == 0 { - // copy until EOF - loop { - // Linux Kernel 5.3 or later - // Not supported in gVisor - // SAFETY: src_fd/dest_fd are valid open fds; copy_file_range is the libc FFI - let written = unsafe { - sys::linux::copy_file_range( - src_fd.native(), - &raw mut off_in_copy, - dest_fd.native(), - &raw mut off_out_copy, - sys::page_size(), - 0, - ) - }; - if let Some(err) = Maybe::::errno_sys_p( - written, - sys::Tag::copy_file_range, - dest, - ) { - match err.get_errno() { - E::EINTR => continue, - E::EXDEV | E::ENOSYS | E::EINVAL | E::EOPNOTSUPP => { - if matches!(err.get_errno(), E::ENOSYS | E::EOPNOTSUPP) { - sys::copy_file::disable_copy_file_range_syscall(); - } - let mut w = wrote.get(); - let r = Self::copy_file_using_sendfile_on_linux_with_read_write_fallback(src, dest, src_fd, dest_fd, size, &mut w); - wrote.set(w); - return r; - } - _ => return err, - } - } - // wrote zero bytes means EOF - if written == 0 { - break; - } - wrote.set(wrote.get().saturating_add(written as u64)); - } - } else { - while size > 0 { - // SAFETY: src_fd/dest_fd are valid open fds; copy_file_range is the libc FFI - let written = unsafe { - sys::linux::copy_file_range( - src_fd.native(), - &raw mut off_in_copy, - dest_fd.native(), - &raw mut off_out_copy, - size, - 0, - ) - }; - if let Some(err) = Maybe::::errno_sys_p( - written, - sys::Tag::copy_file_range, - dest, - ) { - match err.get_errno() { - E::EINTR => continue, - E::EXDEV | E::ENOSYS | E::EINVAL | E::EOPNOTSUPP => { - if matches!(err.get_errno(), E::ENOSYS | E::EOPNOTSUPP) { - sys::copy_file::disable_copy_file_range_syscall(); - } - let mut w = wrote.get(); - let r = Self::copy_file_using_sendfile_on_linux_with_read_write_fallback(src, dest, src_fd, dest_fd, size, &mut w); - wrote.set(w); - return r; - } - _ => return err, - } - } - if written == 0 { - break; - } - wrote.set(wrote.get().saturating_add(written as u64)); - size = size.saturating_sub(written as usize); - } - } - - return Ok(()); + return Self::copy_file_range_with_fallbacks( + src, + dest, + src_fd, + dest_fd, + stat_.st_mode as Mode, + size, + EintrPolicy::Retry, + ); } #[cfg(windows)] @@ -8104,12 +8078,26 @@ impl NodeFS { Maybe::::todo() } - pub fn utimes(&mut self, args: &args::Utimes, _: Flavor) -> Maybe { + /// Shared body of [`Self::utimes`] / [`Self::lutimes`]; they differ only + /// in which syscall applies the timestamps. + fn utimes_with( + &mut self, + args: &args::Utimes, + #[cfg(windows)] uv_utime: unsafe extern "C" fn( + *mut uv::Loop, + *mut uv::fs_t, + *const core::ffi::c_char, + f64, + f64, + uv::uv_fs_cb, + ) -> uv::ReturnCode, + #[cfg(not(windows))] utimens: fn(&ZStr, sys::TimeLike, sys::TimeLike) -> Maybe<()>, + ) -> Maybe { #[cfg(windows)] { let mut req = UvFsReq::new(); let rc = unsafe { - uv::uv_fs_utime( + uv_utime( bun_io::Loop::get(), &mut *req, args.path.slice_z(&mut self.sync_error_buf).as_ptr(), @@ -8130,7 +8118,7 @@ impl NodeFS { }; } #[cfg(not(windows))] - match Syscall::utimens( + match utimens( args.path.slice_z(&mut self.sync_error_buf), to_sys_time_like(args.atime), to_sys_time_like(args.mtime), @@ -8140,40 +8128,18 @@ impl NodeFS { } } + pub fn utimes(&mut self, args: &args::Utimes, _: Flavor) -> Maybe { + #[cfg(windows)] + return self.utimes_with(args, uv::uv_fs_utime); + #[cfg(not(windows))] + self.utimes_with(args, Syscall::utimens) + } + pub fn lutimes(&mut self, args: &args::Lutimes, _: Flavor) -> Maybe { #[cfg(windows)] - { - let mut req = UvFsReq::new(); - let rc = unsafe { - uv::uv_fs_lutime( - bun_io::Loop::get(), - &mut *req, - args.path.slice_z(&mut self.sync_error_buf).as_ptr(), - args.atime, - args.mtime, - None, - ) - }; - return if let Some(errno) = rc.errno() { - Err(sys::Error { - errno, - syscall: sys::Tag::utime, - path: args.path.slice().into(), - ..Default::default() - }) - } else { - Ok(()) - }; - } + return self.utimes_with(args, uv::uv_fs_lutime); #[cfg(not(windows))] - match Syscall::lutimens( - args.path.slice_z(&mut self.sync_error_buf), - to_sys_time_like(args.atime), - to_sys_time_like(args.mtime), - ) { - Err(err) => Err(err.with_path(args.path.slice())), - Ok(_) => Ok(()), - } + self.utimes_with(args, Syscall::lutimens) } pub fn watch(&mut self, args: &args::Watch<'_>, _: Flavor) -> Maybe { @@ -8722,14 +8688,13 @@ impl NodeFS { } let mut flags: i32 = sys::O::CREAT | sys::O::WRONLY; - let wrote: core::cell::Cell = core::cell::Cell::new(0); if mode.shouldnt_overwrite() { flags |= sys::O::EXCL; } let dest_fd = Self::_cp_open_dest_with_mkdir(self, dest, flags)?; - let mut size: usize = stat_.st_size.max(0) as usize; + let size: usize = stat_.st_size.max(0) as usize; if sys::S::ISREG(stat_.st_mode as u32) && sys::copy_file::can_use_ioctl_ficlone() { let rc = sys::linux::ioctl_ficlone(dest_fd, src_fd); @@ -8741,118 +8706,15 @@ impl NodeFS { sys::copy_file::disable_ioctl_ficlone(); } - let _close_dest = scopeguard::guard( - (dest_fd, stat_.st_mode as Mode, &wrote), - |(fd, m, wrote)| { - let _ = Syscall::ftruncate(fd, (wrote.get() & ((1u64 << 63) - 1)) as i64); - let _ = Syscall::fchmod(fd, m); - fd.close(); - }, + return Self::copy_file_range_with_fallbacks( + src, + dest, + src_fd, + dest_fd, + stat_.st_mode as Mode, + size, + EintrPolicy::Surface, ); - - let mut off_in_copy: i64 = 0; - let mut off_out_copy: i64 = 0; - - if !sys::copy_file::can_use_copy_file_range_syscall() { - let mut w = wrote.get(); - let r = Self::copy_file_using_sendfile_on_linux_with_read_write_fallback( - src, dest, src_fd, dest_fd, size, &mut w, - ); - wrote.set(w); - return r; - } - - if size == 0 { - // copy until EOF - loop { - // Linux Kernel 5.3 or later - // Not supported in gVisor - // SAFETY: src_fd/dest_fd are valid open fds; copy_file_range is the libc FFI - let written = unsafe { - sys::linux::copy_file_range( - src_fd.native(), - &raw mut off_in_copy, - dest_fd.native(), - &raw mut off_out_copy, - sys::page_size(), - 0, - ) - }; - if let Some(err) = Maybe::::errno_sys_p( - written, - sys::Tag::copy_file_range, - dest.as_bytes(), - ) { - match err.get_errno() { - // EINVAL: eCryptfs and other filesystems may not support copy_file_range - // XDEV: cross-device copy not supported - // NOSYS: syscall not available - // OPNOTSUPP: filesystem doesn't support this operation - E::EXDEV | E::ENOSYS | E::EINVAL | E::EOPNOTSUPP => { - if matches!(err.get_errno(), E::ENOSYS | E::EOPNOTSUPP) { - sys::copy_file::disable_copy_file_range_syscall(); - } - let mut w = wrote.get(); - let r = Self::copy_file_using_sendfile_on_linux_with_read_write_fallback(src, dest, src_fd, dest_fd, size, &mut w); - wrote.set(w); - return r; - } - _ => return err, - } - } - // wrote zero bytes means EOF - if written == 0 { - break; - } - wrote.set(wrote.get().saturating_add(written as u64)); - } - } else { - while size > 0 { - // Linux Kernel 5.3 or later - // Not supported in gVisor - // SAFETY: src_fd/dest_fd are valid open fds; copy_file_range is the libc FFI - let written = unsafe { - sys::linux::copy_file_range( - src_fd.native(), - &raw mut off_in_copy, - dest_fd.native(), - &raw mut off_out_copy, - size, - 0, - ) - }; - if let Some(err) = Maybe::::errno_sys_p( - written, - sys::Tag::copy_file_range, - dest.as_bytes(), - ) { - match err.get_errno() { - // EINVAL: eCryptfs and other filesystems may not support copy_file_range - // XDEV: cross-device copy not supported - // NOSYS: syscall not available - // OPNOTSUPP: filesystem doesn't support this operation - E::EXDEV | E::ENOSYS | E::EINVAL | E::EOPNOTSUPP => { - if matches!(err.get_errno(), E::ENOSYS | E::EOPNOTSUPP) { - sys::copy_file::disable_copy_file_range_syscall(); - } - let mut w = wrote.get(); - let r = Self::copy_file_using_sendfile_on_linux_with_read_write_fallback(src, dest, src_fd, dest_fd, size, &mut w); - wrote.set(w); - return r; - } - _ => return err, - } - } - // wrote zero bytes means EOF - if written == 0 { - break; - } - wrote.set(wrote.get().saturating_add(written as u64)); - size = size.saturating_sub(written as usize); - } - } - - return Ok(()); } #[cfg(target_os = "freebsd")] @@ -10070,8 +9932,26 @@ fn zig_delete_tree_min_stack_size_with_kind_hint( // ────────────────────────────────────────────────────────────────────────── // NodeFSFunctionEnum — one variant per NodeFS method // ────────────────────────────────────────────────────────────────────────── -#[derive(Copy, Clone, PartialEq, Eq, core::marker::ConstParamTy)] -pub enum NodeFSFunctionEnum { +/// Declares the enum and derives each variant's `"AsyncTask"` heap +/// label from the same list (Rust has no `type_name::()` in `const`, so +/// the label is keyed off the `F` discriminant — each `F` is bound to exactly +/// one `args::*` type via `async_::*`). +macro_rules! node_fs_function_enum { + ($($v:ident),+ $(,)?) => { + #[derive(Copy, Clone, PartialEq, Eq, core::marker::ConstParamTy)] + pub enum NodeFSFunctionEnum { + $($v,)+ + } + + impl NodeFSFunctionEnum { + pub const fn heap_label(self) -> &'static str { + match self { $(Self::$v => concat!("Async", stringify!($v), "Task"),)+ } + } + } + }; +} + +node_fs_function_enum!( Access, AppendFile, Chmod, @@ -10113,7 +9993,7 @@ pub enum NodeFSFunctionEnum { Write, WriteFile, Writev, -} +); impl NodeFSFunctionEnum { /// Maps each async-FS function to its event-loop [`TaskTag`] (the `tags!` @@ -10165,55 +10045,6 @@ impl NodeFSFunctionEnum { } } - /// Heap label `"AsyncTask"`. Rust has no - /// `type_name::()` in `const`, so key off the `F` discriminant - /// (each `F` is bound to exactly one `args::*` type via `async_::*`). - pub const fn heap_label(self) -> &'static str { - macro_rules! lbl { ($($v:ident),+ $(,)?) => { match self { $(Self::$v => concat!("Async", stringify!($v), "Task"),)+ } } } - lbl!( - Access, - AppendFile, - Chmod, - Chown, - Close, - CopyFile, - Exists, - Fchmod, - Fchown, - Fdatasync, - Fstat, - Fsync, - Ftruncate, - Futimes, - Lchmod, - Lchown, - Link, - Lstat, - Lutimes, - Mkdir, - Mkdtemp, - Open, - Read, - Readdir, - ReadFile, - Readlink, - Readv, - Realpath, - RealpathNonNative, - Rename, - Rm, - Rmdir, - Stat, - Statfs, - Symlink, - Truncate, - Unlink, - Utimes, - Write, - WriteFile, - Writev - ) - } pub const fn heap_label_uv(self) -> &'static str { match self { Self::Open => "AsyncOpenUvTask", diff --git a/src/runtime/node/node_zlib_binding.rs b/src/runtime/node/node_zlib_binding.rs index 63b7c71be943..9aacbab90d85 100644 --- a/src/runtime/node/node_zlib_binding.rs +++ b/src/runtime/node/node_zlib_binding.rs @@ -72,6 +72,79 @@ impl Error { } } +/// Placeholder `WorkPoolTask.callback` for the `task` field at construction — +/// `CompressionStream::write` overwrites it before the task is ever scheduled. +/// Safe fn: coerces to the `WorkPoolTask.callback` field type at the +/// struct-init site. +pub(crate) fn unset_task_callback(_: *mut WorkPoolTask) { + unreachable!("WorkPoolTask scheduled before CompressionStream set its callback"); +} + +/// Parses the constructor `mode` argument shared by `Native{Zlib,Brotli,Zstd}`: +/// must be an integer number within the class's `NodeMode` range. +pub(crate) fn validate_mode( + global: &JSGlobalObject, + mode: JSValue, + min: u8, + max: u8, +) -> JsResult { + if !mode.is_number() { + return Err(global.throw_invalid_argument_type_value("mode", "number", mode)); + } + let mode_double = mode.as_number(); + if mode_double % 1.0 != 0.0 { + return Err(global.throw_invalid_argument_type_value("mode", "integer", mode)); + } + let mode_int = mode_double as i64; + if mode_int < i64::from(min) || mode_int > i64::from(max) { + return Err(global.throw_range_error( + mode_int, + jsc::RangeErrorOptions { + field_name: b"mode", + min: i64::from(min), + max: i64::from(max), + msg: b"", + }, + )); + } + Ok(bun_zlib::NodeMode::from_int(mode_int as u8)) +} + +/// Validates that `value` is a `Uint32Array` view and returns it. +pub(crate) fn validate_uint32_array( + global: &JSGlobalObject, + value: JSValue, + name: &str, +) -> JsResult { + let Some(buf) = value.as_array_buffer(global) else { + return Err(global.throw_invalid_argument_type_value(name, "Uint32Array", value)); + }; + if buf.typed_array_type != jsc::JSType::Uint32Array { + return Err(global.throw_invalid_argument_type_value(name, "Uint32Array", value)); + } + Ok(buf) +} + +/// Validates the JS-owned write-result array passed to `init` (`writeResult` / +/// `writeState`): `flush_write_result` writes two u32s into it, so it must be +/// a `Uint32Array` with at least 2 elements. +pub(crate) fn validate_write_result_array( + global: &JSGlobalObject, + value: JSValue, + name: &str, +) -> JsResult<()> { + let mut buf = validate_uint32_array(global, value, name)?; + if buf.as_u32().len() < 2 { + return Err(global + .err( + ErrorCode::INVALID_ARG_VALUE, + format_args!("{name} must be a Uint32Array with at least 2 elements"), + ) + .throw()); + } + Ok(()) +} + // ─── local shims (upstream-crate gaps) ──────────────────────────────────── /// Local `JSValue::toU32` shim — `bun_jsc::JSValue` doesn't expose `to_u32()` @@ -290,102 +363,143 @@ pub(crate) trait CompressionStreamImpl: Sized + Taskable + 'static { fn pending_output_get_cached(this_value: JSValue) -> Option; } -impl CompressionStream { - pub(crate) fn write( - this: &T, - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - let args = callframe.arguments_undef::<7>(); - let arguments = args.slice(); - - if arguments.len() != 7 { - return Err(global_this - .err( - ErrorCode::MISSING_ARGS, - format_args!("write(flush, in, in_off, in_len, out, out_off, out_len)"), - ) - .throw()); - } - - let in_off: u32; - let in_len: u32; - - let this_value = callframe.this(); +/// Validated `write`/`writeSync` arguments: +/// `(flush, in, in_off, in_len, out, out_off, out_len)`. +/// `in_buf` is `None` when the `in` argument is null (flush-only write). The +/// buffers are non-owning views kept alive by the argument `JSValue`s on the +/// caller's frame. +struct WriteArgs { + flush: u32, + in_buf: Option, + in_off: u32, + in_len: u32, + out_buf: jsc::ArrayBuffer, + out_off: u32, + out_len: u32, +} - if arguments[0].is_undefined() { - return Err(global_this - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!("flush value is required"), - ) - .throw()); - } - let flush: u32 = jsv_to_u32(arguments[0]); - if !flush_value_is_valid(flush) { - return Err(global_this - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!("Invalid flush value"), - ) - .throw()); - } +/// Shared 7-argument validation for `write` and `writeSync`; `sig` is the +/// function signature echoed in the missing-args error. +fn parse_write_args( + global_this: &JSGlobalObject, + arguments: &[JSValue], + sig: &str, +) -> JsResult { + if arguments.len() != 7 { + return Err(global_this + .err(ErrorCode::MISSING_ARGS, format_args!("{sig}")) + .throw()); + } - if arguments[1].is_null() { - // just a flush - in_len = 0; - in_off = 0; - } else { - let in_buf = match arguments[1].as_array_buffer(global_this) { - Some(b) => b, - None => { - return Err(global_this - .err( - ErrorCode::INVALID_ARG_TYPE, - format_args!("The \"in\" argument must be a TypedArray or DataView"), - ) - .throw()); - } - }; - in_off = jsv_to_u32(arguments[2]); - in_len = jsv_to_u32(arguments[3]); - if in_buf.byte_len < in_off as usize + in_len as usize { - return Err(global_this - .err( - ErrorCode::OUT_OF_RANGE, - format_args!( - "in_off + in_len ({}) exceeds input buffer length ({})", - in_off as usize + in_len as usize, - in_buf.byte_len, - ), - ) - .throw()); - } - } + if arguments[0].is_undefined() { + return Err(global_this + .err( + ErrorCode::INVALID_ARG_VALUE, + format_args!("flush value is required"), + ) + .throw()); + } + let flush: u32 = jsv_to_u32(arguments[0]); + if !flush_value_is_valid(flush) { + return Err(global_this + .err( + ErrorCode::INVALID_ARG_VALUE, + format_args!("Invalid flush value"), + ) + .throw()); + } - let Some(out_buf) = arguments[4].as_array_buffer(global_this) else { + let in_buf: Option; + let in_off: u32; + let in_len: u32; + if arguments[1].is_null() { + // just a flush + in_buf = None; + in_off = 0; + in_len = 0; + } else { + let Some(buf) = arguments[1].as_array_buffer(global_this) else { return Err(global_this .err( ErrorCode::INVALID_ARG_TYPE, - format_args!("The \"out\" argument must be a TypedArray or DataView"), + format_args!("The \"in\" argument must be a TypedArray or DataView"), ) .throw()); }; - let out_off: u32 = jsv_to_u32(arguments[5]); - let out_len: u32 = jsv_to_u32(arguments[6]); - if out_buf.byte_len < out_off as usize + out_len as usize { + in_off = jsv_to_u32(arguments[2]); + in_len = jsv_to_u32(arguments[3]); + if buf.byte_len < in_off as usize + in_len as usize { return Err(global_this .err( ErrorCode::OUT_OF_RANGE, format_args!( - "out_off + out_len ({}) exceeds output buffer length ({})", - out_off as usize + out_len as usize, - out_buf.byte_len, + "in_off + in_len ({}) exceeds input buffer length ({})", + in_off as usize + in_len as usize, + buf.byte_len, ), ) .throw()); } - let _ = (in_off, in_len, out_off, out_len); + in_buf = Some(buf); + } + + let Some(out_buf) = arguments[4].as_array_buffer(global_this) else { + return Err(global_this + .err( + ErrorCode::INVALID_ARG_TYPE, + format_args!("The \"out\" argument must be a TypedArray or DataView"), + ) + .throw()); + }; + let out_off: u32 = jsv_to_u32(arguments[5]); + let out_len: u32 = jsv_to_u32(arguments[6]); + if out_buf.byte_len < out_off as usize + out_len as usize { + return Err(global_this + .err( + ErrorCode::OUT_OF_RANGE, + format_args!( + "out_off + out_len ({}) exceeds output buffer length ({})", + out_off as usize + out_len as usize, + out_buf.byte_len, + ), + ) + .throw()); + } + + Ok(WriteArgs { + flush, + in_buf, + in_off, + in_len, + out_buf, + out_off, + out_len, + }) +} + +impl CompressionStream { + pub(crate) fn write( + this: &T, + global_this: &JSGlobalObject, + callframe: &CallFrame, + ) -> JsResult { + let args = callframe.arguments_undef::<7>(); + let arguments = args.slice(); + let this_value = callframe.this(); + + let WriteArgs { + flush, + in_buf: in_validated, + in_off, + in_len, + out_off, + out_len, + .. + } = parse_write_args( + global_this, + arguments, + "write(flush, in, in_off, in_len, out, out_off, out_len)", + )?; if this.write_in_progress().get() { return Err(global_this @@ -404,7 +518,7 @@ impl CompressionStream { // FastTypedArray's backing store can fail on OOM, and failing here // leaves nothing to unwind. let in_buf: jsc::ArrayBuffer; - let in_: Option<&[u8]> = if arguments[1].is_null() { + let in_: Option<&[u8]> = if in_validated.is_none() { None } else { let Some(buf) = arguments[1].as_pinned_arraybuffer(global_this) else { @@ -414,7 +528,7 @@ impl CompressionStream { Some(&in_buf.byte_slice()[in_off as usize..in_off as usize + in_len as usize]) }; let Some(mut out_buf) = arguments[4].as_pinned_arraybuffer(global_this) else { - if !arguments[1].is_null() { + if in_validated.is_some() { arguments[1].unpin_array_buffer(); } return Err(global_this.throw_out_of_memory()); @@ -578,103 +692,29 @@ impl CompressionStream { let args = callframe.arguments_undef::<7>(); let arguments = args.slice(); - if arguments.len() != 7 { - return Err(global_this - .err( - ErrorCode::MISSING_ARGS, - format_args!("writeSync(flush, in, in_off, in_len, out, out_off, out_len)"), - ) - .throw()); - } - - let in_off: u32; - let in_len: u32; - let in_: Option<&[u8]>; - - if arguments[0].is_undefined() { - return Err(global_this - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!("flush value is required"), - ) - .throw()); - } - let flush: u32 = jsv_to_u32(arguments[0]); - if !flush_value_is_valid(flush) { - return Err(global_this - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!("Invalid flush value"), - ) - .throw()); - } - - // Hoisted so `in_` can borrow it past the `else` arm (mirrors `out_buf`). - let in_buf: jsc::ArrayBuffer; - if arguments[1].is_null() { - // just a flush - in_ = None; - in_len = 0; - in_off = 0; - } else { - in_buf = match arguments[1].as_array_buffer(global_this) { - Some(b) => b, - None => { - return Err(global_this - .err( - ErrorCode::INVALID_ARG_TYPE, - format_args!("The \"in\" argument must be a TypedArray or DataView"), - ) - .throw()); - } - }; - in_off = jsv_to_u32(arguments[2]); - in_len = jsv_to_u32(arguments[3]); - if in_buf.byte_len < in_off as usize + in_len as usize { - return Err(global_this - .err( - ErrorCode::OUT_OF_RANGE, - format_args!( - "in_off + in_len ({}) exceeds input buffer length ({})", - in_off as usize + in_len as usize, - in_buf.byte_len, - ), - ) - .throw()); - } - // Bounds checked above; `byte_slice` is the safe accessor for the JS - // ArrayBuffer's backing store (rooted via `arguments[1]` on the call stack). - in_ = Some(&in_buf.byte_slice()[in_off as usize..in_off as usize + in_len as usize]); - } - - let Some(mut out_buf) = arguments[4].as_array_buffer(global_this) else { - return Err(global_this - .err( - ErrorCode::INVALID_ARG_TYPE, - format_args!("The \"out\" argument must be a TypedArray or DataView"), - ) - .throw()); - }; - let out_off: u32 = jsv_to_u32(arguments[5]); - let out_len: u32 = jsv_to_u32(arguments[6]); - if out_buf.byte_len < out_off as usize + out_len as usize { - return Err(global_this - .err( - ErrorCode::OUT_OF_RANGE, - format_args!( - "out_off + out_len ({}) exceeds output buffer length ({})", - out_off as usize + out_len as usize, - out_buf.byte_len, - ), - ) - .throw()); - } - // Bounds checked above; `byte_slice_mut` is the safe accessor for the JS - // ArrayBuffer's backing store (rooted via `arguments[4]` on the call stack). + let WriteArgs { + flush, + in_buf, + in_off, + in_len, + mut out_buf, + out_off, + out_len, + } = parse_write_args( + global_this, + arguments, + "writeSync(flush, in, in_off, in_len, out, out_off, out_len)", + )?; + + // Bounds checked in `parse_write_args`; `byte_slice`/`byte_slice_mut` + // are the safe accessors for the JS ArrayBuffers' backing stores + // (rooted via `arguments[1]`/`arguments[4]` on the call stack). + let in_: Option<&[u8]> = in_buf + .as_ref() + .map(|b| &b.byte_slice()[in_off as usize..in_off as usize + in_len as usize]); let out: Option<&mut [u8]> = Some( &mut out_buf.byte_slice_mut()[out_off as usize..out_off as usize + out_len as usize], ); - let _ = (in_off, in_len, out_off, out_len); if this.write_in_progress().get() { return Err(global_this diff --git a/src/runtime/node/path.rs b/src/runtime/node/path.rs index 112e1eea9dc9..abd2bf5ba98e 100644 --- a/src/runtime/node/path.rs +++ b/src/runtime/node/path.rs @@ -328,119 +328,32 @@ pub(crate) fn get_cwd_t(buf: &mut [T]) -> MaybeBuf<'_, T> { // Alias for naming consistency. pub use get_cwd_u8 as get_cwd; -/// Based on Node v21.6.1 path.posix.basename: -/// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L1309 -pub fn basename_posix_t<'a, T: PathCharCwd>(path: &'a [T], suffix: Option<&[T]>) -> &'a [T] { - // validateString of `path` is performed in pub fn basename. - let len = path.len(); - // Exit early for easier number type use. - if len == 0 { - return &[]; - } - let mut start: usize = 0; - // We use an optional value instead of -1, as in Node code, for easier number type use. - let mut end: Option = None; - let mut matched_slash: bool = true; - - let _suffix: &[T] = suffix.unwrap_or(&[]); - let _suffix_len = _suffix.len(); - if suffix.is_some() && _suffix_len > 0 && _suffix_len <= len { - if _suffix == path { - return &[]; - } - // We use an optional value instead of -1, as in Node code, for easier number type use. - let mut ext_idx: Option = Some(_suffix_len - 1); - // We use an optional value instead of -1, as in Node code, for easier number type use. - let mut first_non_slash_end: Option = None; - let mut i_i64 = i64::try_from(len - 1).expect("int cast"); - while i_i64 >= i64::try_from(start).expect("int cast") { - let i = usize::try_from(i_i64).expect("int cast"); - let byte = path[i]; - if byte == T::from_u8(CHAR_FORWARD_SLASH) { - // If we reached a path separator that was not part of a set of path - // separators at the end of the string, stop now - if !matched_slash { - start = i + 1; - break; - } - } else { - if first_non_slash_end.is_none() { - // We saw the first non-path separator, remember this index in case - // we need it if the extension ends up not matching - matched_slash = false; - first_non_slash_end = Some(i + 1); - } - if let Some(_ext_ix) = ext_idx { - // Try to match the explicit extension - if byte == _suffix[_ext_ix] { - if _ext_ix == 0 { - // We matched the extension, so mark this as the end of our path - // component - end = Some(i); - ext_idx = None; - } else { - ext_idx = Some(_ext_ix - 1); - } - } else { - // Extension does not match, so our result is the entire path - // component - ext_idx = None; - end = first_non_slash_end; - } - } - } - i_i64 -= 1; - } - - if let Some(_end) = end { - if start == _end { - return &path[start..first_non_slash_end.unwrap()]; - } else { - return &path[start.._end]; - } - } - return &path[start..len]; - } - - let mut i_i64 = i64::try_from(len - 1).expect("int cast"); - while i_i64 > -1 { - let i = usize::try_from(i_i64).expect("int cast"); - let byte = path[i]; - if byte == T::from_u8(CHAR_FORWARD_SLASH) { - // If we reached a path separator that was not part of a set of path - // separators at the end of the string, stop now - if !matched_slash { - start = i + 1; - break; - } - } else if end.is_none() { - // We saw the first non-path separator, mark this as the end of our - // path component - matched_slash = false; - end = Some(i + 1); - } - i_i64 -= 1; - } - - if let Some(_end) = end { - &path[start.._end] +/// `path.win32` treats both `/` and `\` as separators; `path.posix` only `/`. +/// `IS_WINDOWS` is const so the check monomorphizes to the exact comparison +/// the split posix/win32 functions used. +#[inline(always)] +fn is_sep_t(byte: T) -> bool { + if IS_WINDOWS { + is_sep_windows_t(byte) } else { - &[] + byte == T::from_u8(CHAR_FORWARD_SLASH) } } -/// Based on Node v21.6.1 path.win32.basename: +/// Based on Node v21.6.1 path.posix.basename / path.win32.basename, which +/// differ only in the separator set and the win32 drive-letter prologue: +/// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L1309 /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L753 -pub fn basename_windows_t<'a, T: PathCharCwd>(path: &'a [T], suffix: Option<&[T]>) -> &'a [T] { +fn basename_t<'a, T: PathCharCwd, const IS_WINDOWS: bool>( + path: &'a [T], + suffix: Option<&[T]>, +) -> &'a [T] { // validateString of `path` is performed in pub fn basename. let len = path.len(); // Exit early for easier number type use. if len == 0 { return &[]; } - - let is_sep_t = is_sep_windows_t::; - let mut start: usize = 0; // We use an optional value instead of -1, as in Node code, for easier number type use. let mut end: Option = None; @@ -449,7 +362,11 @@ pub fn basename_windows_t<'a, T: PathCharCwd>(path: &'a [T], suffix: Option<&[T] // Check for a drive letter prefix so as not to mistake the following // path separator as an extra separator at the end of the path that can be // disregarded - if len >= 2 && is_windows_device_root_t(path[0]) && path[1] == T::from_u8(CHAR_COLON) { + if IS_WINDOWS + && len >= 2 + && is_windows_device_root_t(path[0]) + && path[1] == T::from_u8(CHAR_COLON) + { start = 2; } @@ -467,7 +384,7 @@ pub fn basename_windows_t<'a, T: PathCharCwd>(path: &'a [T], suffix: Option<&[T] while i_i64 >= i64::try_from(start).expect("int cast") { let i = usize::try_from(i_i64).expect("int cast"); let byte = path[i]; - if is_sep_t(byte) { + if is_sep_t::(byte) { // If we reached a path separator that was not part of a set of path // separators at the end of the string, stop now if !matched_slash { @@ -517,12 +434,16 @@ pub fn basename_windows_t<'a, T: PathCharCwd>(path: &'a [T], suffix: Option<&[T] while i_i64 >= i64::try_from(start).expect("int cast") { let i = usize::try_from(i_i64).expect("int cast"); let byte = path[i]; - if is_sep_t(byte) { + if is_sep_t::(byte) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now if !matched_slash { start = i + 1; break; } } else if end.is_none() { + // We saw the first non-path separator, mark this as the end of our + // path component matched_slash = false; end = Some(i + 1); } @@ -536,6 +457,14 @@ pub fn basename_windows_t<'a, T: PathCharCwd>(path: &'a [T], suffix: Option<&[T] } } +pub fn basename_posix_t<'a, T: PathCharCwd>(path: &'a [T], suffix: Option<&[T]>) -> &'a [T] { + basename_t::(path, suffix) +} + +pub fn basename_windows_t<'a, T: PathCharCwd>(path: &'a [T], suffix: Option<&[T]>) -> &'a [T] { + basename_t::(path, suffix) +} + pub fn basename_posix_js_t( global_object: &JSGlobalObject, path: &[T], @@ -812,32 +741,39 @@ pub(crate) fn dirname( dirname_js_t::(global_object, is_windows, path_zslice.slice()) } -/// Based on Node v21.6.1 path.posix.extname: -/// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L1388 -pub(crate) fn extname_posix_t(path: &[T]) -> &[T] { - // validateString of `path` is performed in pub fn extname. - let len = path.len(); - // Exit early for easier number type use. - if len == 0 { - return &[]; - } - // We use an optional value instead of -1, as in Node code, for easier number type use. +/// Result of [`scan_last_component_t`]. +struct LastComponentScan { + // We use optional values instead of -1, as in Node code, for easier number type use. + start_dot: Option, + start_part: usize, + end: Option, + pre_dot_state: Option, +} + +/// Reverse scan over the last path component, tracking dot positions. Node +/// repeats this loop verbatim in both the posix and win32 variants of +/// `extname` and `parse`; all four call sites share this copy. +/// +/// `path` must be non-empty; indices below `lower_bound` are not scanned. +fn scan_last_component_t( + path: &[T], + lower_bound: usize, + initial_start_part: usize, +) -> LastComponentScan { let mut start_dot: Option = None; - let mut start_part: usize = 0; - // We use an optional value instead of -1, as in Node code, for easier number type use. + let mut start_part = initial_start_part; let mut end: Option = None; - let mut matched_slash: bool = true; + let mut matched_slash = true; + // Track the state of characters (if any) we see before our first dot and // after any path separator we find - - // We use an optional value instead of -1, as in Node code, for easier number type use. let mut pre_dot_state: Option = Some(0); - let mut i_i64 = i64::try_from(len - 1).expect("int cast"); - while i_i64 > -1 { + let mut i_i64 = i64::try_from(path.len() - 1).expect("int cast"); + while i_i64 >= i64::try_from(lower_bound).expect("int cast") { let i = usize::try_from(i_i64).expect("int cast"); let byte = path[i]; - if byte == T::from_u8(CHAR_FORWARD_SLASH) { + if is_sep_t::(byte) { // If we reached a path separator that was not part of a set of path // separators at the end of the string, stop now if !matched_slash { @@ -847,20 +783,20 @@ pub(crate) fn extname_posix_t(path: &[T]) -> &[T] { i_i64 -= 1; continue; } - if end.is_none() { // We saw the first non-path separator, mark this as the end of our // extension matched_slash = false; end = Some(i + 1); } - if byte == T::from_u8(CHAR_DOT) { // If this is our first dot, mark it as the start of our extension if start_dot.is_none() { start_dot = Some(i); - } else if pre_dot_state.is_some() && pre_dot_state.unwrap() != 1 { - pre_dot_state = Some(1); + } else if let Some(_pre_dot_state) = pre_dot_state { + if _pre_dot_state != 1 { + pre_dot_state = Some(1); + } } } else if start_dot.is_some() { // We saw a non-dot and non-path separator before our dot, so we should @@ -870,25 +806,19 @@ pub(crate) fn extname_posix_t(path: &[T]) -> &[T] { i_i64 -= 1; } - let _end = end.unwrap_or(0); - let _pre_dot_state = pre_dot_state.unwrap_or(0); - let _start_dot = start_dot.unwrap_or(0); - if start_dot.is_none() - || end.is_none() - // We saw a non-dot character immediately before the dot - || (pre_dot_state.is_some() && _pre_dot_state == 0) - // The (right-most) trimmed path component is exactly '..' - || (_pre_dot_state == 1 && _start_dot == _end - 1 && _start_dot == start_part + 1) - { - return &[]; + LastComponentScan { + start_dot, + start_part, + end, + pre_dot_state, } - - &path[_start_dot.._end] } -/// Based on Node v21.6.1 path.win32.extname: +/// Based on Node v21.6.1 path.posix.extname / path.win32.extname, which differ +/// only in the separator set and the win32 drive-letter prologue: +/// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L1388 /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L840 -pub(crate) fn extname_windows_t(path: &[T]) -> &[T] { +fn extname_t(path: &[T]) -> &[T] { // validateString of `path` is performed in pub fn extname. let len = path.len(); // Exit early for easier number type use. @@ -896,63 +826,24 @@ pub(crate) fn extname_windows_t(path: &[T]) -> &[T] { return &[]; } let mut start: usize = 0; - // We use an optional value instead of -1, as in Node code, for easier number type use. - let mut start_dot: Option = None; - let mut start_part: usize = 0; - // We use an optional value instead of -1, as in Node code, for easier number type use. - let mut end: Option = None; - let mut matched_slash: bool = true; - // Track the state of characters (if any) we see before our first dot and - // after any path separator we find - - // We use an optional value instead of -1, as in Node code, for easier number type use. - let mut pre_dot_state: Option = Some(0); // Check for a drive letter prefix so as not to mistake the following // path separator as an extra separator at the end of the path that can be // disregarded - - if len >= 2 && path[1] == T::from_u8(CHAR_COLON) && is_windows_device_root_t(path[0]) { + if IS_WINDOWS + && len >= 2 + && path[1] == T::from_u8(CHAR_COLON) + && is_windows_device_root_t(path[0]) + { start = 2; - start_part = start; } - let mut i_i64 = i64::try_from(len - 1).expect("int cast"); - while i_i64 >= i64::try_from(start).expect("int cast") { - let i = usize::try_from(i_i64).expect("int cast"); - let byte = path[i]; - if is_sep_windows_t(byte) { - // If we reached a path separator that was not part of a set of path - // separators at the end of the string, stop now - if !matched_slash { - start_part = i + 1; - break; - } - i_i64 -= 1; - continue; - } - if end.is_none() { - // We saw the first non-path separator, mark this as the end of our - // extension - matched_slash = false; - end = Some(i + 1); - } - if byte == T::from_u8(CHAR_DOT) { - // If this is our first dot, mark it as the start of our extension - if start_dot.is_none() { - start_dot = Some(i); - } else if let Some(_pre_dot_state) = pre_dot_state { - if _pre_dot_state != 1 { - pre_dot_state = Some(1); - } - } - } else if start_dot.is_some() { - // We saw a non-dot and non-path separator before our dot, so we should - // have a good chance at having a non-empty extension - pre_dot_state = None; - } - i_i64 -= 1; - } + let LastComponentScan { + start_dot, + start_part, + end, + pre_dot_state, + } = scan_last_component_t::(path, start, start); let _end = end.unwrap_or(0); let _pre_dot_state = pre_dot_state.unwrap_or(0); @@ -970,6 +861,14 @@ pub(crate) fn extname_windows_t(path: &[T]) -> &[T] { &path[_start_dot.._end] } +pub(crate) fn extname_posix_t(path: &[T]) -> &[T] { + extname_t::(path) +} + +pub(crate) fn extname_windows_t(path: &[T]) -> &[T] { + extname_t::(path) +} + pub use bun_paths::is_sep_posix_t; // Node `path.win32.isPathSeparator` accepts BOTH `/` and `\` — semantically // `is_sep_any_t`, NOT `is_sep_win32_t` (which is `\`-only). Keep the Node name. @@ -2045,56 +1944,13 @@ pub fn parse_posix_t(path: &[T]) -> PathParsed<'_, T> { start = 1; } - // We use an optional value instead of -1, as in Node code, for easier number type use. - let mut start_dot: Option = None; - let mut start_part: usize = 0; - // We use an optional value instead of -1, as in Node code, for easier number type use. - let mut end: Option = None; - let mut matched_slash = true; - let mut i_i64 = i64::try_from(len - 1).expect("int cast"); - - // Track the state of characters (if any) we see before our first dot and - // after any path separator we find - - // We use an optional value instead of -1, as in Node code, for easier number type use. - let mut pre_dot_state: Option = Some(0); - // Get non-dir info - while i_i64 >= i64::try_from(start).expect("int cast") { - let i = usize::try_from(i_i64).expect("int cast"); - let byte = path[i]; - if byte == T::from_u8(CHAR_FORWARD_SLASH) { - // If we reached a path separator that was not part of a set of path - // separators at the end of the string, stop now - if !matched_slash { - start_part = i + 1; - break; - } - i_i64 -= 1; - continue; - } - if end.is_none() { - // We saw the first non-path separator, mark this as the end of our - // extension - matched_slash = false; - end = Some(i + 1); - } - if byte == T::from_u8(CHAR_DOT) { - // If this is our first dot, mark it as the start of our extension - if start_dot.is_none() { - start_dot = Some(i); - } else if let Some(_pre_dot_state) = pre_dot_state { - if _pre_dot_state != 1 { - pre_dot_state = Some(1); - } - } - } else if start_dot.is_some() { - // We saw a non-dot and non-path separator before our dot, so we should - // have a good chance at having a non-empty extension - pre_dot_state = None; - } - i_i64 -= 1; - } + let LastComponentScan { + start_dot, + start_part, + end, + pre_dot_state, + } = scan_last_component_t::(path, start, 0); if let Some(_end) = end { let _pre_dot_state = pre_dot_state.unwrap_or(0); @@ -2159,7 +2015,7 @@ pub fn parse_windows_t(path: &[T]) -> PathParsed<'_, T> { let is_sep_t = is_sep_windows_t::; let mut root_end: usize = 0; - let mut byte = path[0]; + let byte = path[0]; if len == 1 { if is_sep_t(byte) { @@ -2254,56 +2110,13 @@ pub fn parse_windows_t(path: &[T]) -> PathParsed<'_, T> { root = &path[0..root_end]; } - // We use an optional value instead of -1, as in Node code, for easier number type use. - let mut start_dot: Option = None; - let mut start_part = root_end; - // We use an optional value instead of -1, as in Node code, for easier number type use. - let mut end: Option = None; - let mut matched_slash = true; - let mut i_i64 = i64::try_from(len - 1).expect("int cast"); - - // Track the state of characters (if any) we see before our first dot and - // after any path separator we find - - // We use an optional value instead of -1, as in Node code, for easier number type use. - let mut pre_dot_state: Option = Some(0); - // Get non-dir info - while i_i64 >= i64::try_from(root_end).expect("int cast") { - let i = usize::try_from(i_i64).expect("int cast"); - byte = path[i]; - if is_sep_t(byte) { - // If we reached a path separator that was not part of a set of path - // separators at the end of the string, stop now - if !matched_slash { - start_part = i + 1; - break; - } - i_i64 -= 1; - continue; - } - if end.is_none() { - // We saw the first non-path separator, mark this as the end of our - // extension - matched_slash = false; - end = Some(i + 1); - } - if byte == T::from_u8(CHAR_DOT) { - // If this is our first dot, mark it as the start of our extension - if start_dot.is_none() { - start_dot = Some(i); - } else if let Some(_pre_dot_state) = pre_dot_state { - if _pre_dot_state != 1 { - pre_dot_state = Some(1); - } - } - } else if start_dot.is_some() { - // We saw a non-dot and non-path separator before our dot, so we should - // have a good chance at having a non-empty extension - pre_dot_state = None; - } - i_i64 -= 1; - } + let LastComponentScan { + start_dot, + start_part, + end, + pre_dot_state, + } = scan_last_component_t::(path, root_end, root_end); if let Some(_end) = end { let _pre_dot_state = pre_dot_state.unwrap_or(0); diff --git a/src/runtime/node/types.rs b/src/runtime/node/types.rs index a5979c4335b1..e0f41b7e48e8 100644 --- a/src/runtime/node/types.rs +++ b/src/runtime/node/types.rs @@ -439,20 +439,7 @@ impl StringOrBuffer { Ok(true) } - JSType::ArrayBuffer - | JSType::Int8Array - | JSType::Uint8Array - | JSType::Uint8ClampedArray - | JSType::Int16Array - | JSType::Uint16Array - | JSType::Int32Array - | JSType::Uint32Array - | JSType::Float32Array - | JSType::Float16Array - | JSType::Float64Array - | JSType::BigInt64Array - | JSType::BigUint64Array - | JSType::DataView => { + t if t.is_array_buffer_like() => { let buffer = if is_async { Buffer::from_js_pinned(global, value) .unwrap_or_else(|| Buffer::from_array_buffer(global, value)) diff --git a/src/runtime/node/zlib/NativeBrotli.rs b/src/runtime/node/zlib/NativeBrotli.rs index 9aee4a90d5dd..9b6cc7b7dfc6 100644 --- a/src/runtime/node/zlib/NativeBrotli.rs +++ b/src/runtime/node/zlib/NativeBrotli.rs @@ -53,11 +53,14 @@ mod _impl { use core::ffi::c_uint; use bun_jsc::{ - CallFrame, ErrorCode, JSGlobalObject, JSValue, JsCell, JsResult, RangeErrorOptions, - StrongOptional, WorkPoolTask, + CallFrame, ErrorCode, JSGlobalObject, JSValue, JsCell, JsResult, StrongOptional, + WorkPoolTask, }; - use crate::node::node_zlib_binding::{CompressionStream, CountedKeepAlive, Error}; + use crate::node::node_zlib_binding::{ + CompressionStream, CountedKeepAlive, Error, unset_task_callback, validate_mode, + validate_uint32_array, validate_write_result_array, + }; use crate::node::util::validators; // Intrusive refcount: the handle type is `bun_ptr::IntrusiveRc`; the @@ -110,28 +113,7 @@ mod _impl { ) -> JsResult> { let arguments = callframe.arguments_undef::<1>(); - let mode = arguments.ptr[0]; - if !mode.is_number() { - return Err(global_this.throw_invalid_argument_type_value("mode", "number", mode)); - } - let mode_double = mode.as_number(); - if mode_double % 1.0 != 0.0 { - return Err(global_this.throw_invalid_argument_type_value("mode", "integer", mode)); - } - let mode_int: i64 = mode_double as i64; - if mode_int < 8 || mode_int > 9 { - return Err(global_this.throw_range_error( - mode_int, - RangeErrorOptions { - field_name: b"mode", - min: 8, - max: 9, - ..Default::default() - }, - )); - } - - let mode = bun_zlib::NodeMode::from_int(mode_int as u8); + let mode = validate_mode(global_this, arguments.ptr[0], 8, 9)?; let stream = Context { mode, ..Default::default() @@ -150,7 +132,7 @@ mod _impl { // .callback = undefined — overwritten before WorkPool::schedule() task: JsCell::new(WorkPoolTask { node: Default::default(), - callback: noop_task_callback, + callback: unset_task_callback, }), estimated_external_size: Self::external_size_for(mode), })) @@ -190,53 +172,15 @@ mod _impl { .throw()); } - // `flush_write_result` writes two u32s into this array, so the - // caller-supplied array must hold at least 2 elements. let write_result_value = arguments.ptr[1]; - let Some(mut write_result_buf) = write_result_value.as_array_buffer(global_this) else { - return Err(global_this.throw_invalid_argument_type_value( - "writeResult", - "Uint32Array", - write_result_value, - )); - }; - if write_result_buf.typed_array_type != bun_jsc::JSType::Uint32Array { - return Err(global_this.throw_invalid_argument_type_value( - "writeResult", - "Uint32Array", - write_result_value, - )); - } - let write_result_slice = write_result_buf.as_u32(); - if write_result_slice.len() < 2 { - return Err(global_this - .err( - ErrorCode::INVALID_ARG_VALUE, - format_args!("writeResult must be a Uint32Array with at least 2 elements"), - ) - .throw()); - } + validate_write_result_array(global_this, write_result_value, "writeResult")?; let write_callback = validators::validate_function(global_this, "writeCallback", arguments.ptr[2])?; // Validate `params` before any native state is initialized so the // error path needs no cleanup. `as_u32` reinterprets the view's // bytes, so the element type must actually be Uint32Array. - let params_value = arguments.ptr[0]; - let Some(mut params_buf) = params_value.as_array_buffer(global_this) else { - return Err(global_this.throw_invalid_argument_type_value( - "params", - "Uint32Array", - params_value, - )); - }; - if params_buf.typed_array_type != bun_jsc::JSType::Uint32Array { - return Err(global_this.throw_invalid_argument_type_value( - "params", - "Uint32Array", - params_value, - )); - } + let mut params_buf = validate_uint32_array(global_this, arguments.ptr[0], "params")?; js::write_result_set_cached(this_value, global_this, write_result_value); @@ -607,12 +551,6 @@ mod _impl { s.as_ptr() } - /// Placeholder for `WorkPoolTask.callback` — overwritten before scheduling - /// (see `CompressionStream::write` in node_zlib_binding.rs). - /// Safe fn: coerces to the `WorkPoolTask.callback` field type at the - /// struct-init site; the body never dereferences the pointer. - fn noop_task_callback(_task: *mut WorkPoolTask) {} - crate::__compression_stream_mixin_reexports!(NativeBrotli); } // mod _impl diff --git a/src/runtime/node/zlib/NativeZlib.rs b/src/runtime/node/zlib/NativeZlib.rs index f452a45204ed..e46ddd5d8165 100644 --- a/src/runtime/node/zlib/NativeZlib.rs +++ b/src/runtime/node/zlib/NativeZlib.rs @@ -19,15 +19,12 @@ mod _impl { CallFrame, JSGlobalObject, JSValue, JsCell, JsResult, StrongOptional, WorkPoolTask, }; - use crate::node::node_zlib_binding::{CompressionStream, CountedKeepAlive}; + use crate::node::node_zlib_binding::{ + CompressionStream, CountedKeepAlive, unset_task_callback, validate_mode, + validate_write_result_array, + }; use crate::node::util::validators; - /// Placeholder for `WorkPoolTask.callback` — overwritten before scheduling - /// (see `CompressionStream::write` in node_zlib_binding.rs). - /// Safe fn: coerces to the `WorkPoolTask.callback` field type at the - /// struct-init site; the body never dereferences the pointer. - fn noop_task_callback(_task: *mut WorkPoolTask) {} - // `mod js { write_callback_*, error_callback_*, dictionary_* }` is emitted by // `__impl_compression_stream!` below (wraps `bun_jsc::codegen_cached_accessors!`). @@ -65,29 +62,9 @@ mod _impl { pub fn constructor(global: &JSGlobalObject, frame: &CallFrame) -> JsResult> { let arguments = frame.arguments_undef::<4>(); - let mode = arguments.ptr[0]; - if !mode.is_number() { - return Err(global.throw_invalid_argument_type_value("mode", "number", mode)); - } - let mode_double = mode.as_number(); - if mode_double % 1.0 != 0.0 { - return Err(global.throw_invalid_argument_type_value("mode", "integer", mode)); - } - let mode_int: i64 = mode_double as i64; - if mode_int < 1 || mode_int > 7 { - return Err(global.throw_range_error( - mode_int, - bun_jsc::RangeErrorOptions { - field_name: b"mode", - min: 1, - max: 7, - msg: b"", - }, - )); - } - + let mode = validate_mode(global, arguments.ptr[0], 1, 7)?; let stream = Context { - mode: c::NodeMode::from_int(mode_int as u8), + mode, ..Default::default() }; Ok(Box::new(Self { @@ -103,7 +80,7 @@ mod _impl { closed: Cell::new(false), task: JsCell::new(WorkPoolTask { node: Default::default(), - callback: noop_task_callback, + callback: unset_task_callback, }), })) } @@ -138,32 +115,8 @@ mod _impl { validators::validate_int32(global, arguments.ptr[2], "memLevel", None, None)?; let strategy = validators::validate_int32(global, arguments.ptr[3], "strategy", None, None)?; - // `flush_write_result` writes two u32s into this array, so the - // caller-supplied array must hold at least 2 elements. let write_result_value = arguments.ptr[4]; - let Some(mut write_result_buf) = write_result_value.as_array_buffer(global) else { - return Err(global.throw_invalid_argument_type_value( - "writeResult", - "Uint32Array", - write_result_value, - )); - }; - if write_result_buf.typed_array_type != bun_jsc::JSType::Uint32Array { - return Err(global.throw_invalid_argument_type_value( - "writeResult", - "Uint32Array", - write_result_value, - )); - } - let write_result_slice = write_result_buf.as_u32(); - if write_result_slice.len() < 2 { - return Err(global - .err( - bun_jsc::ErrorCode::INVALID_ARG_VALUE, - format_args!("writeResult must be a Uint32Array with at least 2 elements"), - ) - .throw()); - } + validate_write_result_array(global, write_result_value, "writeResult")?; let write_callback = validators::validate_function(global, "writeCallback", arguments.ptr[5])?; // Bind the ArrayBuffer view to a local so the borrowed byte_slice() outlives diff --git a/src/runtime/node/zlib/NativeZstd.rs b/src/runtime/node/zlib/NativeZstd.rs index 2e6227071738..ad4aab37f28b 100644 --- a/src/runtime/node/zlib/NativeZstd.rs +++ b/src/runtime/node/zlib/NativeZstd.rs @@ -11,7 +11,10 @@ mod _impl { }; use bun_zstd::c; // `bun.c` translated-c-headers (ZSTD_* fns/consts live here) - use crate::node::node_zlib_binding::{CompressionStream, CountedKeepAlive, Error}; + use crate::node::node_zlib_binding::{ + CompressionStream, CountedKeepAlive, Error, unset_task_callback, validate_mode, + validate_uint32_array, validate_write_result_array, + }; use crate::node::util::validators; // #[repr(u8)] enum shared by all native-zlib stream types. use bun_zlib::NodeMode; @@ -21,14 +24,6 @@ mod _impl { // `NativeZstdPrototype__${prop}{Get,Set}CachedValue` C++ symbols emitted by // `src/codegen/generate-classes.ts` for `values: [...]` in `zlib.classes.ts`. - /// Placeholder WorkPoolTask callback — overwritten by CompressionStream::write - /// before the task is ever scheduled. - /// Safe fn: coerces to the `WorkPoolTask.callback` field type at the - /// struct-init site; the body never dereferences the pointer. - fn unset_task_callback(_: *mut WorkPoolTask) { - unreachable!("WorkPoolTask scheduled before CompressionStream set its callback"); - } - // R-2 (host-fn re-entrancy): every JS-exposed method takes `&self`; per-field // interior mutability via `Cell` (Copy) / `JsCell` (non-Copy). The codegen // `host_fn_this` shim still passes `&mut NativeZstd` — `&mut T` auto-reborrows @@ -75,28 +70,7 @@ mod _impl { pub fn constructor(global: &JSGlobalObject, frame: &CallFrame) -> JsResult> { let arguments = frame.arguments_as_array::<1>(); - let mode = arguments[0]; - if !mode.is_number() { - return Err(global.throw_invalid_argument_type_value("mode", "number", mode)); - } - let mode_double = mode.as_number(); - if mode_double % 1.0 != 0.0 { - return Err(global.throw_invalid_argument_type_value("mode", "integer", mode)); - } - let mode_int: i64 = mode_double as i64; - if mode_int < 10 || mode_int > 11 { - return Err(global.throw_range_error( - mode_int, - jsc::RangeErrorOptions { - field_name: b"mode", - min: 10, - max: 11, - msg: b"", - }, - )); - } - - let mode = NodeMode::from_int(mode_int as u8); + let mode = validate_mode(global, arguments[0], 10, 11)?; let stream = Context { mode, ..Default::default() @@ -158,31 +132,7 @@ mod _impl { let write_state_value = arguments[2]; let process_callback_value = arguments[3]; - let Some(mut write_state) = write_state_value.as_array_buffer(global) else { - return Err(global.throw_invalid_argument_type_value( - "writeState", - "Uint32Array", - write_state_value, - )); - }; - if write_state.typed_array_type != jsc::JSType::Uint32Array { - return Err(global.throw_invalid_argument_type_value( - "writeState", - "Uint32Array", - write_state_value, - )); - } - // `flush_write_result` writes two u32s into this array, so the - // caller-supplied array must hold at least 2 elements. - let write_state_slice = write_state.as_u32(); - if write_state_slice.len() < 2 { - return Err(global - .err( - jsc::ErrorCode::INVALID_ARG_VALUE, - format_args!("writeState must be a Uint32Array with at least 2 elements"), - ) - .throw()); - } + validate_write_result_array(global, write_state_value, "writeState")?; js::write_result_set_cached(this_value, global, write_state_value); let write_js_callback = @@ -210,20 +160,8 @@ mod _impl { return Ok(JSValue::FALSE); } - let Some(mut params_) = init_params_array_value.as_array_buffer(global) else { - return Err(global.throw_invalid_argument_type_value( - "initParamsArray", - "Uint32Array", - init_params_array_value, - )); - }; - if params_.typed_array_type != jsc::JSType::Uint32Array { - return Err(global.throw_invalid_argument_type_value( - "initParamsArray", - "Uint32Array", - init_params_array_value, - )); - } + let mut params_ = + validate_uint32_array(global, init_params_array_value, "initParamsArray")?; for (i, &x) in params_.as_u32().iter().enumerate() { if x == u32::MAX { continue; diff --git a/src/runtime/server/ServerWebSocket.rs b/src/runtime/server/ServerWebSocket.rs index 5fb0c0e297b4..87d832fb7aac 100644 --- a/src/runtime/server/ServerWebSocket.rs +++ b/src/runtime/server/ServerWebSocket.rs @@ -221,11 +221,9 @@ impl ServerWebSocket { // guard on `compress` even when compress is args[2] (long-standing // user-visible behavior; do not "fix"). // - // A unified `publish_prologue` covering the full callframe header was - // considered and rejected: publishText omits the empty-topic check and - // reuses "publish" in its min-args message (both user-visible), so a single - // prologue would either change user-visible errors or carry per-caller - // bool flags — net more code than three small orthogonal helpers. + // `publish_prologue` parameterizes the two per-method divergences: + // publishText omits the empty-topic check and reuses "publish" in its + // min-args message and debug logs (both user-visible; do not "fix"). // ────────────────────────────────────────────────────────────────────── /// `(app, ssl, publish_to_self)` from the handler, or `None` when the @@ -264,6 +262,60 @@ impl ServerWebSocket { Ok(args_len > 1 && compress_value.to_boolean()) } + /// Shared prologue for `publish`/`publishText`/`publishBinary`: min-arity + /// check, closed-server check, topic validation, and compress parsing. + /// `Ok(None)` means the server is closed (caller returns `0`). + /// + /// `fn_name` is the method name used in error messages; `log_name` is the + /// name used in debug logs and the min-args message (`publishText` reports + /// "publish" there) and `require_non_empty_topic` is `false` only for + /// `publishText` — both long-standing user-visible behavior; do not "fix". + #[inline] + fn publish_prologue( + &self, + global_this: &JSGlobalObject, + callframe: &CallFrame, + fn_name: &'static str, + log_name: &'static str, + require_non_empty_topic: bool, + ) -> JsResult> { + let args = callframe.arguments_old::<4>(); + if args.len < 1 { + bun_output::scoped_log!(WebSocketServer, "{}()", log_name); + return Err(global_this.throw(format_args!("{log_name} requires at least 1 argument"))); + } + + let Some((app, ssl, publish_to_self)) = self.publish_ctx() else { + bun_output::scoped_log!(WebSocketServer, "publish() closed"); + return Ok(None); + }; + + let topic_value = args.ptr[0]; + let message_value = args.ptr[1]; + let compress_value = args.ptr[2]; + + if topic_value.is_empty_or_undefined_or_null() || !topic_value.is_string() { + bun_output::scoped_log!(WebSocketServer, "{}() topic invalid", log_name); + return Err(global_this.throw(format_args!("{fn_name} requires a topic string"))); + } + + let topic_slice = topic_value.to_slice(global_this)?; + if require_non_empty_topic && topic_slice.slice().is_empty() { + return Err(global_this.throw(format_args!("{fn_name} requires a non-empty topic"))); + } + + let compress = Self::parse_compress_arg(global_this, fn_name, compress_value, args.len)?; + + Ok(Some(( + app, + ssl, + publish_to_self, + topic_slice, + message_value, + compress, + ))) + } + /// Route a publish through either the per-socket uWS handle (when /// `!publish_to_self && !closed`) or the app-wide broadcast, then map the /// bool result to the JS number contract: success → `len & 0x7FFF_FFFF`, @@ -764,33 +816,12 @@ impl ServerWebSocket { global_this: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let args = callframe.arguments_old::<4>(); - if args.len < 1 { - bun_output::scoped_log!(WebSocketServer, "publish()"); - return Err(global_this.throw(format_args!("publish requires at least 1 argument"))); - } - - let Some((app, ssl, publish_to_self)) = self.publish_ctx() else { - bun_output::scoped_log!(WebSocketServer, "publish() closed"); + let Some((app, ssl, publish_to_self, topic_slice, message_value, compress)) = + self.publish_prologue(global_this, callframe, "publish", "publish", true)? + else { return Ok(JSValue::js_number(0.0)); }; - let topic_value = args.ptr[0]; - let message_value = args.ptr[1]; - let compress_value = args.ptr[2]; - - if topic_value.is_empty_or_undefined_or_null() || !topic_value.is_string() { - bun_output::scoped_log!(WebSocketServer, "publish() topic invalid"); - return Err(global_this.throw(format_args!("publish requires a topic string"))); - } - - let topic_slice = topic_value.to_slice(global_this)?; - if topic_slice.slice().is_empty() { - return Err(global_this.throw(format_args!("publish requires a non-empty topic"))); - } - - let compress = Self::parse_compress_arg(global_this, "publish", compress_value, args.len)?; - if message_value.is_empty_or_undefined_or_null() { return Err(global_this.throw(format_args!("publish requires a non-empty message"))); } @@ -833,32 +864,12 @@ impl ServerWebSocket { global_this: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let args = callframe.arguments_old::<4>(); - - if args.len < 1 { - bun_output::scoped_log!(WebSocketServer, "publish()"); - return Err(global_this.throw(format_args!("publish requires at least 1 argument"))); - } - - let Some((app, ssl, publish_to_self)) = self.publish_ctx() else { - bun_output::scoped_log!(WebSocketServer, "publish() closed"); + let Some((app, ssl, publish_to_self, topic_slice, message_value, compress)) = + self.publish_prologue(global_this, callframe, "publishText", "publish", false)? + else { return Ok(JSValue::js_number(0.0)); }; - let topic_value = args.ptr[0]; - let message_value = args.ptr[1]; - let compress_value = args.ptr[2]; - - if topic_value.is_empty_or_undefined_or_null() || !topic_value.is_string() { - bun_output::scoped_log!(WebSocketServer, "publish() topic invalid"); - return Err(global_this.throw(format_args!("publishText requires a topic string"))); - } - - let topic_slice = topic_value.to_slice(global_this)?; - - let compress = - Self::parse_compress_arg(global_this, "publishText", compress_value, args.len)?; - if message_value.is_empty_or_undefined_or_null() || !message_value.is_string() { return Err(global_this.throw(format_args!("publishText requires a non-empty message"))); } @@ -886,35 +897,17 @@ impl ServerWebSocket { global_this: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let args = callframe.arguments_old::<4>(); - - if args.len < 1 { - bun_output::scoped_log!(WebSocketServer, "publishBinary()"); - return Err( - global_this.throw(format_args!("publishBinary requires at least 1 argument")) - ); - } - - let Some((app, ssl, publish_to_self)) = self.publish_ctx() else { - bun_output::scoped_log!(WebSocketServer, "publish() closed"); + let Some((app, ssl, publish_to_self, topic_slice, message_value, compress)) = self + .publish_prologue( + global_this, + callframe, + "publishBinary", + "publishBinary", + true, + )? + else { return Ok(JSValue::js_number(0.0)); }; - let topic_value = args.ptr[0]; - let message_value = args.ptr[1]; - let compress_value = args.ptr[2]; - - if topic_value.is_empty_or_undefined_or_null() || !topic_value.is_string() { - bun_output::scoped_log!(WebSocketServer, "publishBinary() topic invalid"); - return Err(global_this.throw(format_args!("publishBinary requires a topic string"))); - } - - let topic_slice = topic_value.to_slice(global_this)?; - if topic_slice.slice().is_empty() { - return Err(global_this.throw(format_args!("publishBinary requires a non-empty topic"))); - } - - let compress = - Self::parse_compress_arg(global_this, "publishBinary", compress_value, args.len)?; if message_value.is_empty_or_undefined_or_null() { return Err( diff --git a/src/runtime/shell/builtin/basename.rs b/src/runtime/shell/builtin/basename.rs index 221c95717cde..6a0983c1baaa 100644 --- a/src/runtime/shell/builtin/basename.rs +++ b/src/runtime/shell/builtin/basename.rs @@ -3,10 +3,17 @@ use crate::shell::interpreter::{Interpreter, NodeId}; use crate::shell::io_writer::{ChildPtr, WriterTag}; use crate::shell::yield_::Yield; +/// One-argument-per-line path transform shared by `basename` and `dirname`. +pub trait PathTransform: Default { + const KIND: Kind; + fn apply(path: &[u8]) -> &[u8]; +} + #[derive(Default)] -pub struct Basename { +pub struct PathBuiltin { state: State, buf: Vec, + _transform: std::marker::PhantomData, } #[derive(Default)] @@ -17,17 +24,32 @@ enum State { Done, } -impl Basename { - pub(crate) fn start(interp: &Interpreter, cmd: NodeId) -> Yield { +#[derive(Default)] +pub struct BasenameTransform; + +impl PathTransform for BasenameTransform { + const KIND: Kind = Kind::Basename; + fn apply(path: &[u8]) -> &[u8] { + bun_paths::resolve_path::basename(path) + } +} + +pub type Basename = PathBuiltin; + +impl PathBuiltin { + pub(crate) fn start(interp: &Interpreter, cmd: NodeId) -> Yield + where + Self: BuiltinState, + { let buf = { let bltn = Builtin::of(interp, cmd); let argc = bltn.args_slice().len(); if argc == 0 { - return Self::fail(interp, cmd, Kind::Basename.usage_string()); + return Self::fail(interp, cmd, T::KIND.usage_string()); } let mut buf = Vec::new(); for i in 0..argc { - buf.extend_from_slice(bun_paths::resolve_path::basename(bltn.arg_bytes(i))); + buf.extend_from_slice(T::apply(bltn.arg_bytes(i))); buf.push(b'\n'); } buf @@ -46,7 +68,10 @@ impl Basename { Builtin::done(interp, cmd, 0) } - fn fail(interp: &Interpreter, cmd: NodeId, msg: &[u8]) -> Yield { + fn fail(interp: &Interpreter, cmd: NodeId, msg: &[u8]) -> Yield + where + Self: BuiltinState, + { Self::state_mut(interp, cmd).state = State::Err; Builtin::write_failing_error(interp, cmd, msg, 1) } @@ -56,7 +81,10 @@ impl Basename { cmd: NodeId, _: usize, err: Option, - ) -> Yield { + ) -> Yield + where + Self: BuiltinState, + { if let Some(e) = err { e.deref(); Self::state_mut(interp, cmd).state = State::Err; @@ -65,7 +93,7 @@ impl Basename { match Self::state_mut(interp, cmd).state { State::Done => Builtin::done(interp, cmd, 0), State::Err => Builtin::done(interp, cmd, 1), - State::Idle => unreachable!("Basename.onIOWriterChunk: idle"), + State::Idle => unreachable!("{}.onIOWriterChunk: idle", T::KIND.as_str()), } } } diff --git a/src/runtime/shell/builtin/cp.rs b/src/runtime/shell/builtin/cp.rs index 854e2bb415fb..9cbd5509701f 100644 --- a/src/runtime/shell/builtin/cp.rs +++ b/src/runtime/shell/builtin/cp.rs @@ -3,7 +3,7 @@ use bun_paths::resolve_path; use crate::shell::builtin::{Builtin, BuiltinState, IoKind, Kind}; use crate::shell::interpreter::{ EventLoopHandle, FlagParser, Interpreter, NodeId, OutputSrc, OutputTask, OutputTaskVTable, - ParseFlagResult, ShellTask, parse_flags, unsupported_flag, + ParseFlagResult, ShellTask, impl_output_task_vtable, parse_flags, unsupported_flag, }; use crate::shell::io_writer::{ChildPtr, WriterTag}; use crate::shell::yield_::Yield; @@ -193,23 +193,6 @@ impl Cp { } } - pub(crate) fn on_io_writer_chunk( - interp: &Interpreter, - cmd: NodeId, - written: usize, - e: Option, - ) -> Yield { - if matches!(Self::state_mut(interp, cmd).state, State::WaitingWriteErr) { - return Builtin::done(interp, cmd, 1); - } - if let Some(task) = Self::state_mut(interp, cmd).output_queue.pop_front() { - // SAFETY: `task` was heap-allocated in `OutputTask::new` and - // pushed by `write_err`/`write_out`; not yet freed. - return unsafe { OutputTask::::on_io_writer_chunk(task, interp, written, e) }; - } - Self::next(interp, cmd) - } - /// Windows-only post-processing of tasks that failed with EBUSY: if some /// other task already succeeded /// for the same absolute src/tgt, the EBUSY is benign and the task is @@ -320,67 +303,7 @@ impl Cp { } } -impl OutputTaskVTable for Cp { - fn write_err( - interp: &Interpreter, - cmd: NodeId, - child: *mut OutputTask, - errbuf: &[u8], - ) -> Option { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_waiting += 1; - } - if let Some(safeguard) = Builtin::of(interp, cmd).stderr.needs_io() { - // Stash so on_io_writer_chunk can route to the OutputTask state - // machine and reclaim the box (stopgap for missing WriterTag). - Self::state_mut(interp, cmd).output_queue.push_back(child); - let childptr = ChildPtr::new(cmd, WriterTag::Builtin); - return Some( - Builtin::of_mut(interp, cmd) - .stderr - .enqueue(childptr, errbuf, safeguard), - ); - } - let _ = Builtin::write_no_io(interp, cmd, IoKind::Stderr, errbuf); - None - } - fn on_write_err(interp: &Interpreter, cmd: NodeId) { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_done += 1; - } - } - fn write_out( - interp: &Interpreter, - cmd: NodeId, - child: *mut OutputTask, - output: &mut OutputSrc, - ) -> Option { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_waiting += 1; - } - if let Some(safeguard) = Builtin::of(interp, cmd).stdout.needs_io() { - Self::state_mut(interp, cmd).output_queue.push_back(child); - let childptr = ChildPtr::new(cmd, WriterTag::Builtin); - let buf = output.slice().to_vec(); - return Some( - Builtin::of_mut(interp, cmd) - .stdout - .enqueue(childptr, &buf, safeguard), - ); - } - let buf = output.slice().to_vec(); - let _ = Builtin::write_no_io(interp, cmd, IoKind::Stdout, &buf); - None - } - fn on_write_out(interp: &Interpreter, cmd: NodeId) { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_done += 1; - } - } - fn on_done(interp: &Interpreter, cmd: NodeId) -> Yield { - Self::next(interp, cmd) - } -} +impl_output_task_vtable!(Cp, queue_on_self); /// Resolves src/tgt to absolute paths, decides /// which POSIX `cp` synopsis applies, then hands off to the node:fs async cp diff --git a/src/runtime/shell/builtin/dirname.rs b/src/runtime/shell/builtin/dirname.rs index 4edfdb2db591..af1660702b14 100644 --- a/src/runtime/shell/builtin/dirname.rs +++ b/src/runtime/shell/builtin/dirname.rs @@ -1,74 +1,15 @@ -use crate::shell::builtin::{Builtin, BuiltinState, IoKind}; -use crate::shell::interpreter::{Interpreter, NodeId}; -use crate::shell::io_writer::{ChildPtr, WriterTag}; -use crate::shell::yield_::Yield; +use crate::shell::builtin::Kind; +use crate::shell::builtins::basename::{PathBuiltin, PathTransform}; #[derive(Default)] -pub struct Dirname { - state: State, - buf: Vec, -} - -#[derive(Default)] -enum State { - #[default] - Idle, - Err, - Done, -} - -impl Dirname { - pub(crate) fn start(interp: &Interpreter, cmd: NodeId) -> Yield { - let bltn = Builtin::of(interp, cmd); - let argc = bltn.args_slice().len(); - if argc == 0 { - return Self::fail(interp, cmd, b"usage: dirname string\n"); - } - - let stdout_needs_io = bltn.stdout.needs_io(); - let mut buf = Vec::new(); - for i in 0..argc { - let path = bltn.arg_bytes(i); - let dir = bun_paths::resolve_path::dirname::(path); - let dir: &[u8] = if dir.is_empty() { b"." } else { dir }; - buf.extend_from_slice(dir); - buf.push(b'\n'); - } +pub struct DirnameTransform; - Self::state_mut(interp, cmd).state = State::Done; - if let Some(safeguard) = stdout_needs_io { - Self::state_mut(interp, cmd).buf = buf; - let owned = Self::state_mut(interp, cmd).buf.clone(); - let child = ChildPtr::new(cmd, WriterTag::Builtin); - return Builtin::of_mut(interp, cmd) - .stdout - .enqueue(child, &owned, safeguard); - } - let _ = Builtin::write_no_io(interp, cmd, IoKind::Stdout, &buf); - Builtin::done(interp, cmd, 0) - } - - fn fail(interp: &Interpreter, cmd: NodeId, msg: &[u8]) -> Yield { - Self::state_mut(interp, cmd).state = State::Err; - Builtin::write_failing_error(interp, cmd, msg, 1) - } - - pub(crate) fn on_io_writer_chunk( - interp: &Interpreter, - cmd: NodeId, - _: usize, - err: Option, - ) -> Yield { - if let Some(e) = err { - e.deref(); - Self::state_mut(interp, cmd).state = State::Err; - return Builtin::done(interp, cmd, 1); - } - let exit = match Self::state_mut(interp, cmd).state { - State::Done => 0, - State::Err => 1, - State::Idle => unreachable!("Dirname.onIOWriterChunk: idle"), - }; - Builtin::done(interp, cmd, exit) +impl PathTransform for DirnameTransform { + const KIND: Kind = Kind::Dirname; + fn apply(path: &[u8]) -> &[u8] { + let dir = bun_paths::resolve_path::dirname::(path); + if dir.is_empty() { b"." } else { dir } } } + +pub type Dirname = PathBuiltin; diff --git a/src/runtime/shell/builtin/ls.rs b/src/runtime/shell/builtin/ls.rs index 978adf9b9d8d..53af6c312221 100644 --- a/src/runtime/shell/builtin/ls.rs +++ b/src/runtime/shell/builtin/ls.rs @@ -9,7 +9,7 @@ use crate::shell::ExitCode; use crate::shell::builtin::{Builtin, IoKind, Kind}; use crate::shell::interpreter::{ EventLoopHandle, Interpreter, NodeId, OutputSrc, OutputTask, OutputTaskVTable, ShellTask, - shell_openat, + impl_output_task_vtable, shell_openat, }; use crate::shell::io_writer::{ChildPtr, WriterTag}; use crate::shell::yield_::Yield; @@ -184,28 +184,6 @@ impl Ls { } } - pub(crate) fn on_io_writer_chunk( - interp: &Interpreter, - cmd: NodeId, - written: usize, - e: Option, - ) -> Yield { - if matches!(Self::state_mut(interp, cmd).state, State::WaitingWriteErr) { - return Builtin::done(interp, cmd, 1); - } - let pending = if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_queue.pop_front() - } else { - None - }; - if let Some(task) = pending { - // SAFETY: `task` was heap-allocated in `OutputTask::new` and - // pushed by `write_err`/`write_out`; not yet freed. - return unsafe { OutputTask::::on_io_writer_chunk(task, interp, written, e) }; - } - Self::next(interp, cmd) - } - /// # Safety /// `task` must be a live heap allocation produced by /// [`ShellLsTask::create`]; ownership is reclaimed here. @@ -290,71 +268,7 @@ impl Ls { } } -impl OutputTaskVTable for Ls { - fn write_err( - interp: &Interpreter, - cmd: NodeId, - child: *mut OutputTask, - errbuf: &[u8], - ) -> Option { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_waiting += 1; - } - if let Some(safeguard) = Builtin::of(interp, cmd).stderr.needs_io() { - // Stash so on_io_writer_chunk can route to the OutputTask state - // machine and reclaim the box (stopgap for missing WriterTag). - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_queue.push_back(child); - } - let childptr = ChildPtr::new(cmd, WriterTag::Builtin); - return Some( - Builtin::of_mut(interp, cmd) - .stderr - .enqueue(childptr, errbuf, safeguard), - ); - } - let _ = Builtin::write_no_io(interp, cmd, IoKind::Stderr, errbuf); - None - } - fn on_write_err(interp: &Interpreter, cmd: NodeId) { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_done += 1; - } - } - fn write_out( - interp: &Interpreter, - cmd: NodeId, - child: *mut OutputTask, - output: &mut OutputSrc, - ) -> Option { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_waiting += 1; - } - if let Some(safeguard) = Builtin::of(interp, cmd).stdout.needs_io() { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_queue.push_back(child); - } - let childptr = ChildPtr::new(cmd, WriterTag::Builtin); - let buf = output.slice().to_vec(); - return Some( - Builtin::of_mut(interp, cmd) - .stdout - .enqueue(childptr, &buf, safeguard), - ); - } - let buf = output.slice().to_vec(); - let _ = Builtin::write_no_io(interp, cmd, IoKind::Stdout, &buf); - None - } - fn on_write_out(interp: &Interpreter, cmd: NodeId) { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_done += 1; - } - } - fn on_done(interp: &Interpreter, cmd: NodeId) -> Yield { - Self::next(interp, cmd) - } -} +impl_output_task_vtable!(Ls, queue_in_exec); #[derive(Clone, Copy, Default)] pub enum ResultKind { diff --git a/src/runtime/shell/builtin/mkdir.rs b/src/runtime/shell/builtin/mkdir.rs index a47d53443ff8..05c8163f6948 100644 --- a/src/runtime/shell/builtin/mkdir.rs +++ b/src/runtime/shell/builtin/mkdir.rs @@ -4,7 +4,7 @@ use crate::shell::ExitCode; use crate::shell::builtin::{Builtin, BuiltinState, IoKind, Kind}; use crate::shell::interpreter::{ EventLoopHandle, FlagParser, Interpreter, NodeId, OutputSrc, OutputTask, OutputTaskVTable, - ParseFlagResult, ShellTask, parse_flags, unsupported_flag, + ParseFlagResult, ShellTask, impl_output_task_vtable, parse_flags, unsupported_flag, }; use crate::shell::io_writer::{ChildPtr, WriterTag}; use crate::shell::yield_::Yield; @@ -137,25 +137,6 @@ impl Mkdir { } } - pub(crate) fn on_io_writer_chunk( - interp: &Interpreter, - cmd: NodeId, - written: usize, - e: Option, - ) -> Yield { - let pending = match &mut Self::state_mut(interp, cmd).state { - State::WaitingWriteErr => return Builtin::done(interp, cmd, 1), - State::Exec(exec) => exec.output_queue.pop_front(), - State::Idle | State::Done => panic!("Invalid state"), - }; - if let Some(task) = pending { - // SAFETY: `task` was heap-allocated in `OutputTask::new` and - // pushed by `write_err`/`write_out`; not yet freed. - return unsafe { OutputTask::::on_io_writer_chunk(task, interp, written, e) }; - } - Self::next(interp, cmd) - } - /// The caller ([`ShellMkdirTask::run_from_main_thread`]) owns the heap /// allocation and drops it after this returns. pub(crate) fn on_shell_mkdir_task_done( @@ -186,80 +167,7 @@ enum NextAction { Schedule(usize), } -impl OutputTaskVTable for Mkdir { - fn write_err( - interp: &Interpreter, - cmd: NodeId, - child: *mut OutputTask, - errbuf: &[u8], - ) -> Option { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_waiting += 1; - } - if let Some(safeguard) = Builtin::of(interp, cmd).stderr.needs_io() { - // OutputTask has no `WriterTag` of its own (it is not directly - // dispatchable as an IOWriter child), so the enqueue is tagged - // `WriterTag::Builtin` and `child` is stashed on `output_queue`; - // `on_io_writer_chunk` pops it to route the completion back to - // the OutputTask state machine and reclaim the box. - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_queue.push_back(child); - } - let childptr = ChildPtr::new(cmd, WriterTag::Builtin); - return Some( - Builtin::of_mut(interp, cmd) - .stderr - .enqueue(childptr, errbuf, safeguard), - ); - } - let _ = Builtin::write_no_io(interp, cmd, IoKind::Stderr, errbuf); - None - } - - fn on_write_err(interp: &Interpreter, cmd: NodeId) { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_done += 1; - } - } - - fn write_out( - interp: &Interpreter, - cmd: NodeId, - child: *mut OutputTask, - output: &mut OutputSrc, - ) -> Option { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_waiting += 1; - } - if let Some(safeguard) = Builtin::of(interp, cmd).stdout.needs_io() { - // See write_err — stash `child` so the chunk callback routes to - // OutputTask::on_io_writer_chunk. - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_queue.push_back(child); - } - let childptr = ChildPtr::new(cmd, WriterTag::Builtin); - let buf = output.slice().to_vec(); - return Some( - Builtin::of_mut(interp, cmd) - .stdout - .enqueue(childptr, &buf, safeguard), - ); - } - let buf = output.slice().to_vec(); - let _ = Builtin::write_no_io(interp, cmd, IoKind::Stdout, &buf); - None - } - - fn on_write_out(interp: &Interpreter, cmd: NodeId) { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_done += 1; - } - } - - fn on_done(interp: &Interpreter, cmd: NodeId) -> Yield { - Self::next(interp, cmd) - } -} +impl_output_task_vtable!(Mkdir, queue_in_exec); /// Runs `mkdir`/`mkdir -p` on a worker /// thread, then bounces back to the main thread. diff --git a/src/runtime/shell/builtin/touch.rs b/src/runtime/shell/builtin/touch.rs index 6faa724faa13..d5499bae8767 100644 --- a/src/runtime/shell/builtin/touch.rs +++ b/src/runtime/shell/builtin/touch.rs @@ -2,7 +2,7 @@ use crate::shell::ExitCode; use crate::shell::builtin::{Builtin, BuiltinState, IoKind, Kind}; use crate::shell::interpreter::{ EventLoopHandle, FlagParser, Interpreter, NodeId, OutputSrc, OutputTask, OutputTaskVTable, - ParseFlagResult, ShellTask, parse_flags, unsupported_flag, + ParseFlagResult, ShellTask, impl_output_task_vtable, parse_flags, unsupported_flag, }; use crate::shell::io_writer::{ChildPtr, WriterTag}; use crate::shell::yield_::Yield; @@ -126,28 +126,6 @@ impl Touch { } } - pub(crate) fn on_io_writer_chunk( - interp: &Interpreter, - cmd: NodeId, - written: usize, - e: Option, - ) -> Yield { - if matches!(Self::state_mut(interp, cmd).state, State::WaitingWriteErr) { - return Builtin::done(interp, cmd, 1); - } - let pending = if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_queue.pop_front() - } else { - None - }; - if let Some(task) = pending { - // SAFETY: `task` was heap-allocated in `OutputTask::new` and - // pushed by `write_err`/`write_out`; not yet freed. - return unsafe { OutputTask::::on_io_writer_chunk(task, interp, written, e) }; - } - Self::next(interp, cmd) - } - /// # Safety /// `task` must be a live heap allocation produced by /// [`ShellTouchTask::create`]; ownership is reclaimed here. @@ -174,71 +152,7 @@ impl Touch { } } -impl OutputTaskVTable for Touch { - fn write_err( - interp: &Interpreter, - cmd: NodeId, - child: *mut OutputTask, - errbuf: &[u8], - ) -> Option { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_waiting += 1; - } - if let Some(safeguard) = Builtin::of(interp, cmd).stderr.needs_io() { - // Stash so on_io_writer_chunk can route to the OutputTask state - // machine and reclaim the box (stopgap for missing WriterTag). - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_queue.push_back(child); - } - let childptr = ChildPtr::new(cmd, WriterTag::Builtin); - return Some( - Builtin::of_mut(interp, cmd) - .stderr - .enqueue(childptr, errbuf, safeguard), - ); - } - let _ = Builtin::write_no_io(interp, cmd, IoKind::Stderr, errbuf); - None - } - fn on_write_err(interp: &Interpreter, cmd: NodeId) { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_done += 1; - } - } - fn write_out( - interp: &Interpreter, - cmd: NodeId, - child: *mut OutputTask, - output: &mut OutputSrc, - ) -> Option { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_waiting += 1; - } - if let Some(safeguard) = Builtin::of(interp, cmd).stdout.needs_io() { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_queue.push_back(child); - } - let childptr = ChildPtr::new(cmd, WriterTag::Builtin); - let buf = output.slice().to_vec(); - return Some( - Builtin::of_mut(interp, cmd) - .stdout - .enqueue(childptr, &buf, safeguard), - ); - } - let buf = output.slice().to_vec(); - let _ = Builtin::write_no_io(interp, cmd, IoKind::Stdout, &buf); - None - } - fn on_write_out(interp: &Interpreter, cmd: NodeId) { - if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - exec.output_done += 1; - } - } - fn on_done(interp: &Interpreter, cmd: NodeId) -> Yield { - Self::next(interp, cmd) - } -} +impl_output_task_vtable!(Touch, queue_in_exec); /// utimes() the path (creating it on ENOENT) on a worker thread. pub struct ShellTouchTask { diff --git a/src/runtime/shell/interpreter.rs b/src/runtime/shell/interpreter.rs index 693b33fa798c..5b208c8083f2 100644 --- a/src/runtime/shell/interpreter.rs +++ b/src/runtime/shell/interpreter.rs @@ -2773,6 +2773,147 @@ impl OutputTask

{ } } +/// Stamps out the boilerplate [`OutputTaskVTable`] impl shared by the +/// task-based builtins (cp/ls/mkdir/touch): bump +/// `output_waiting`/`output_done` on the `Exec` state, stash the task on the +/// builtin's `output_queue` when the write goes through the IOWriter, and +/// fall back to `write_no_io` otherwise. Also stamps out the builtin's +/// `on_io_writer_chunk`, which pops the queue to route the chunk completion +/// back to the OutputTask state machine. +/// +/// The second argument selects where `output_queue` lives: +/// - `queue_in_exec` — on the `State::Exec` payload (ls/mkdir/touch) +/// - `queue_on_self` — directly on the builtin struct (cp; see the field +/// comment there) +/// +/// Expands in the builtin's module, so `State`, `Builtin`, etc. resolve to +/// the caller's imports and the builtin's own `state_mut`/`next` are used. +macro_rules! impl_output_task_vtable { + ($builtin:ident, queue_in_exec) => { + impl $builtin { + #[inline] + fn output_queue_push(interp: &Interpreter, cmd: NodeId, child: *mut OutputTask) { + if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { + exec.output_queue.push_back(child); + } + } + #[inline] + fn output_queue_pop(interp: &Interpreter, cmd: NodeId) -> Option<*mut OutputTask> { + if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { + exec.output_queue.pop_front() + } else { + None + } + } + } + crate::shell::interpreter::impl_output_task_vtable!(@impl $builtin); + }; + ($builtin:ident, queue_on_self) => { + impl $builtin { + #[inline] + fn output_queue_push(interp: &Interpreter, cmd: NodeId, child: *mut OutputTask) { + Self::state_mut(interp, cmd).output_queue.push_back(child); + } + #[inline] + fn output_queue_pop(interp: &Interpreter, cmd: NodeId) -> Option<*mut OutputTask> { + Self::state_mut(interp, cmd).output_queue.pop_front() + } + } + crate::shell::interpreter::impl_output_task_vtable!(@impl $builtin); + }; + (@impl $builtin:ident) => { + impl $builtin { + pub(crate) fn on_io_writer_chunk( + interp: &Interpreter, + cmd: NodeId, + written: usize, + e: Option, + ) -> Yield { + if matches!(Self::state_mut(interp, cmd).state, State::WaitingWriteErr) { + return Builtin::done(interp, cmd, 1); + } + if let Some(task) = Self::output_queue_pop(interp, cmd) { + // SAFETY: `task` was heap-allocated in `OutputTask::new` and + // pushed by `write_err`/`write_out`; not yet freed. + return unsafe { + OutputTask::::on_io_writer_chunk(task, interp, written, e) + }; + } + Self::next(interp, cmd) + } + } + impl OutputTaskVTable for $builtin { + fn write_err( + interp: &Interpreter, + cmd: NodeId, + child: *mut OutputTask, + errbuf: &[u8], + ) -> Option { + if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { + exec.output_waiting += 1; + } + if let Some(safeguard) = Builtin::of(interp, cmd).stderr.needs_io() { + // OutputTask has no `WriterTag` of its own (it is not + // directly dispatchable as an IOWriter child), so the + // enqueue is tagged `WriterTag::Builtin` and `child` is + // stashed on `output_queue`; the builtin's + // `on_io_writer_chunk` pops it to route the completion + // back to the OutputTask state machine and reclaim the + // box. + Self::output_queue_push(interp, cmd, child); + let childptr = ChildPtr::new(cmd, WriterTag::Builtin); + return Some( + Builtin::of_mut(interp, cmd) + .stderr + .enqueue(childptr, errbuf, safeguard), + ); + } + let _ = Builtin::write_no_io(interp, cmd, IoKind::Stderr, errbuf); + None + } + fn on_write_err(interp: &Interpreter, cmd: NodeId) { + if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { + exec.output_done += 1; + } + } + fn write_out( + interp: &Interpreter, + cmd: NodeId, + child: *mut OutputTask, + output: &mut OutputSrc, + ) -> Option { + if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { + exec.output_waiting += 1; + } + if let Some(safeguard) = Builtin::of(interp, cmd).stdout.needs_io() { + // See write_err — stash `child` so the chunk callback + // routes to OutputTask::on_io_writer_chunk. + Self::output_queue_push(interp, cmd, child); + let childptr = ChildPtr::new(cmd, WriterTag::Builtin); + let buf = output.slice().to_vec(); + return Some( + Builtin::of_mut(interp, cmd) + .stdout + .enqueue(childptr, &buf, safeguard), + ); + } + let buf = output.slice().to_vec(); + let _ = Builtin::write_no_io(interp, cmd, IoKind::Stdout, &buf); + None + } + fn on_write_out(interp: &Interpreter, cmd: NodeId) { + if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { + exec.output_done += 1; + } + } + fn on_done(interp: &Interpreter, cmd: NodeId) -> Yield { + Self::next(interp, cmd) + } + } + }; +} +pub(crate) use impl_output_task_vtable; + // ──────────────────────────────────────────────────────────────────────────── // ShellTask // ──────────────────────────────────────────────────────────────────────────── diff --git a/src/runtime/shell/shell_body.rs b/src/runtime/shell/shell_body.rs index 858fb9769f0a..a1dc213ac8f3 100644 --- a/src/runtime/shell/shell_body.rs +++ b/src/runtime/shell/shell_body.rs @@ -1108,17 +1108,14 @@ pub mod testing_apis { } } - /// Codegen (`generated_js2native.rs`) wraps this with `host_fn_result`, so we - /// expose the bare `JsHostFnZig` signature here and do the buffer scope inline. - pub fn shell_lex(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - MarkedArgumentBuffer::new(|buf| shell_lex_impl(global, callframe, buf)) - } - - fn shell_lex_impl( + /// Shared prologue for the lex/parse testing APIs: extract the two + /// arguments (template strings + interpolated values) and assemble the + /// shell source via `shell_cmd_from_js`. + fn shell_cmd_args_from_js( global: &JSGlobalObject, callframe: &CallFrame, marked_argument_buffer: &mut MarkedArgumentBuffer, - ) -> JsResult { + ) -> JsResult<(Bump, JsStrings, Vec, Vec)> { let arguments_ = callframe.arguments_old::<2>(); // SAFETY: bun_vm() is non-null for a Bun-owned global. let vm = global.bun_vm(); @@ -1142,7 +1139,6 @@ pub mod testing_apis { let mut jsstrings = JsStrings::with_capacity(4); // SAFETY: every JSValue pushed here is also rooted in marked_argument_buffer. let mut jsobjs: Vec = Vec::new(); - let mut script: Vec = Vec::new(); shell_cmd_from_js( global, @@ -1153,6 +1149,22 @@ pub mod testing_apis { &mut script, marked_argument_buffer, )?; + Ok((arena, jsstrings, jsobjs, script)) + } + + /// Codegen (`generated_js2native.rs`) wraps this with `host_fn_result`, so we + /// expose the bare `JsHostFnZig` signature here and do the buffer scope inline. + pub fn shell_lex(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { + MarkedArgumentBuffer::new(|buf| shell_lex_impl(global, callframe, buf)) + } + + fn shell_lex_impl( + global: &JSGlobalObject, + callframe: &CallFrame, + marked_argument_buffer: &mut MarkedArgumentBuffer, + ) -> JsResult { + let (arena, mut jsstrings, jsobjs, script) = + shell_cmd_args_from_js(global, callframe, marked_argument_buffer)?; let jsobjs_len: u32 = u32::try_from(jsobjs.len()).expect("int cast"); let lex_result = 'brk: { @@ -1199,39 +1211,8 @@ pub mod testing_apis { callframe: &CallFrame, marked_argument_buffer: &mut MarkedArgumentBuffer, ) -> JsResult { - let arguments_ = callframe.arguments_old::<2>(); - // SAFETY: bun_vm() is non-null for a Bun-owned global. - let vm = global.bun_vm(); - let mut arguments = jsc::ArgumentsSlice::init(vm, arguments_.slice()); - let string_args: JSValue = match arguments.next_eat() { - Some(s) => s, - None => { - return Err(global.throw(format_args!("shell_parse: expected 2 arguments, got 0"))); - } - }; - - let arena = Bump::new(); - - let template_args_js: JSValue = match arguments.next_eat() { - Some(s) => s, - None => { - return Err(global.throw(format_args!("shell: expected 2 arguments, got 0"))); - } - }; - let mut template_args = template_args_js.array_iterator(global)?; - let mut jsstrings = JsStrings::with_capacity(4); - // SAFETY: every JSValue pushed here is also rooted in marked_argument_buffer. - let mut jsobjs: Vec = Vec::new(); - let mut script: Vec = Vec::new(); - shell_cmd_from_js( - global, - string_args, - &mut template_args, - &mut jsobjs, - &mut jsstrings, - &mut script, - marked_argument_buffer, - )?; + let (arena, mut jsstrings, mut jsobjs, script) = + shell_cmd_args_from_js(global, callframe, marked_argument_buffer)?; let mut out_parser: Option> = None; let mut out_lex_result: Option> = None; diff --git a/src/runtime/shell/subproc.rs b/src/runtime/shell/subproc.rs index 7051305d3e7a..201567330fa1 100644 --- a/src/runtime/shell/subproc.rs +++ b/src/runtime/shell/subproc.rs @@ -1025,10 +1025,10 @@ impl Writable { // match (E0509). Dispatch on `&mut` and `mem::take` / ManuallyDrop the // non-Copy payloads. let mut stdio = stdio; - #[cfg(windows)] - { - match &mut stdio { - Stdio::Pipe | Stdio::ReadableStream(_) => { + match &mut stdio { + Stdio::Pipe | Stdio::ReadableStream(_) => { + #[cfg(windows)] + { if let StdioResult::Buffer(buf) = result { // Ownership of the `Box` transfers into the // FileSink's writer. @@ -1057,93 +1057,39 @@ impl Writable { // owned ref; `adopt` takes it over. return Ok(Writable::Pipe(unsafe { FileSinkPtr::adopt(pipe_ptr) })); } - return Ok(Writable::Inherit); - } - - Stdio::Blob(_) => { - // E0509: `Stdio` impls `Drop`, so the payload cannot be - // destructure-moved out. Take ownership via ManuallyDrop + - // ptr::read; the wrapper suppresses the Stdio destructor so - // the blob is moved exactly once. - let old = - core::mem::ManuallyDrop::new(core::mem::replace(&mut stdio, Stdio::Ignore)); - // SAFETY: `old` is Blob (matched above) and ManuallyDrop - // prevents its Drop from running, so this is the sole move. - let blob = match &*old { - Stdio::Blob(b) => unsafe { core::ptr::read(b) }, - _ => unreachable!(), - }; - return Ok(Writable::Buffer(StaticPipeWriter::create( - event_loop, - subprocess, - result, - JscSubprocess::source_from_blob(blob), - ))); + Ok(Writable::Inherit) } - Stdio::ArrayBuffer(array_buffer) => { - return Ok(Writable::Buffer(StaticPipeWriter::create( - event_loop, - subprocess, - result, - JscSubprocess::source_from_array_buffer(core::mem::take(array_buffer)), - ))); - } - Stdio::Fd(fd) => { - return Ok(Writable::Fd(*fd)); - } - Stdio::Dup2(dup2) => { - return Ok(Writable::Fd(dup2.to.to_fd())); - } - Stdio::Inherit => { - return Ok(Writable::Inherit); - } - Stdio::Memfd(_) | Stdio::Path(_) | Stdio::Ignore => { - return Ok(Writable::Ignore); - } - Stdio::Ipc | Stdio::Capture(_) => { - return Ok(Writable::Ignore); + #[cfg(not(windows))] + { + // The shell never uses this + panic!("Unimplemented stdin pipe/readable_stream"); } } - } - #[cfg(not(windows))] - { - match &mut stdio { - Stdio::Dup2(_) => { - // The shell never uses this - panic!("Unimplemented stdin dup2"); + Stdio::Blob(_) | Stdio::ArrayBuffer(_) => Ok(Writable::Buffer( + JscSubprocess::writable::buffered_stdin_writer( + &mut stdio, event_loop, subprocess, result, + ), + )), + Stdio::Dup2(dup2) => { + #[cfg(windows)] + { + Ok(Writable::Fd(dup2.to.to_fd())) } - Stdio::Pipe => { + #[cfg(not(windows))] + { + let _ = dup2; // The shell never uses this - panic!("Unimplemented stdin pipe"); + panic!("Unimplemented stdin dup2"); } - - Stdio::Blob(_) => { - // E0509: `Stdio` impls `Drop`, so the payload cannot be - // destructure-moved out. Take ownership via ManuallyDrop + - // ptr::read; the wrapper suppresses the Stdio destructor so - // the blob is moved exactly once. - let old = - core::mem::ManuallyDrop::new(core::mem::replace(&mut stdio, Stdio::Ignore)); - let blob = match &*old { - // SAFETY: `old` is Blob (matched above) and ManuallyDrop - // prevents its Drop from running, so this is the sole move. - Stdio::Blob(b) => unsafe { core::ptr::read(b) }, - _ => unreachable!(), - }; - Ok(Writable::Buffer(StaticPipeWriter::create( - event_loop, - subprocess, - result, - JscSubprocess::source_from_blob(blob), - ))) + } + Stdio::Memfd(memfd) => { + #[cfg(windows)] + { + let _ = memfd; + Ok(Writable::Ignore) } - Stdio::ArrayBuffer(array_buffer) => Ok(Writable::Buffer(StaticPipeWriter::create( - event_loop, - subprocess, - result, - JscSubprocess::source_from_array_buffer(core::mem::take(array_buffer)), - ))), - Stdio::Memfd(memfd) => { + #[cfg(not(windows))] + { debug_assert!(memfd.is_valid()); let fd = *memfd; // Ownership of the fd transfers to `Writable::Memfd`. @@ -1155,15 +1101,20 @@ impl Writable { core::mem::ManuallyDrop::new(core::mem::replace(&mut stdio, Stdio::Ignore)); Ok(Writable::Memfd(fd)) } - Stdio::Fd(_) => Ok(Writable::Fd(result.unwrap())), - Stdio::Inherit => Ok(Writable::Inherit), - Stdio::Path(_) | Stdio::Ignore => Ok(Writable::Ignore), - Stdio::Ipc | Stdio::Capture(_) => Ok(Writable::Ignore), - Stdio::ReadableStream(_) => { - // The shell never uses this - panic!("Unimplemented stdin readable_stream"); + } + Stdio::Fd(fd) => { + #[cfg(windows)] + { + Ok(Writable::Fd(*fd)) + } + #[cfg(not(windows))] + { + let _ = fd; + Ok(Writable::Fd(result.unwrap())) } } + Stdio::Inherit => Ok(Writable::Inherit), + Stdio::Path(_) | Stdio::Ignore | Stdio::Ipc | Stdio::Capture(_) => Ok(Writable::Ignore), } } @@ -1288,52 +1239,32 @@ impl Readable { // Note: `Stdio` impls Drop, so dispatch on `&mut` and `mem::take` // Default-able payloads instead of partial moves (E0509). let mut stdio = stdio; - #[cfg(windows)] - { - return match &mut stdio { - Stdio::Inherit => Readable::Inherit, - Stdio::Ipc | Stdio::Dup2(_) | Stdio::Ignore => Readable::Ignore, - Stdio::Path(_) => Readable::Ignore, - Stdio::Fd(fd) => Readable::Fd(*fd), - // blobs are immutable, so we should only ever get the case - // where the user passed in a Blob with an fd - Stdio::Blob(_) => Readable::Ignore, - Stdio::Memfd(_) => Readable::Ignore, - Stdio::Pipe => Readable::Pipe(PipeReader::create( - event_loop, process, result, None, out_type, interp, - )), - Stdio::ArrayBuffer(array_buffer) => { - let mut pipe = - PipeReader::create(event_loop, process, result, None, out_type, interp); - // The Arc was just created by `PipeReader::create` and is - // uniquely held (strong=1, weak=0) — `get_mut` is the - // safe route to set `buffered_output` before it's shared. - Arc::get_mut(&mut pipe) - .expect("fresh PipeReader Arc") - .buffered_output = BufferedOutput::ArrayBuffer { - buf: core::mem::take(array_buffer), - i: 0, - }; - Readable::Pipe(pipe) + match &mut stdio { + Stdio::Inherit => Readable::Inherit, + Stdio::Ipc | Stdio::Dup2(_) | Stdio::Ignore => Readable::Ignore, + Stdio::Path(_) => Readable::Ignore, + Stdio::Fd(fd) => { + #[cfg(windows)] + { + Readable::Fd(*fd) } - Stdio::Capture(_) => Readable::Pipe(PipeReader::create( - event_loop, process, result, shellio, out_type, interp, - )), - Stdio::ReadableStream(_) => Readable::Ignore, // Shell doesn't use readable_stream - }; - } - - #[cfg(not(windows))] - { - match &mut stdio { - Stdio::Inherit => Readable::Inherit, - Stdio::Ipc | Stdio::Dup2(_) | Stdio::Ignore => Readable::Ignore, - Stdio::Path(_) => Readable::Ignore, - Stdio::Fd(_) => Readable::Fd(result.unwrap()), - // blobs are immutable, so we should only ever get the case - // where the user passed in a Blob with an fd - Stdio::Blob(_) => Readable::Ignore, - Stdio::Memfd(memfd) => { + #[cfg(not(windows))] + { + let _ = fd; + Readable::Fd(result.unwrap()) + } + } + // blobs are immutable, so we should only ever get the case + // where the user passed in a Blob with an fd + Stdio::Blob(_) => Readable::Ignore, + Stdio::Memfd(memfd) => { + #[cfg(windows)] + { + let _ = memfd; + Readable::Ignore + } + #[cfg(not(windows))] + { let fd = *memfd; // Ownership of the fd transfers to `Readable::Memfd`. Swap in // `Ignore` and suppress the old value's destructor so @@ -1342,28 +1273,28 @@ impl Readable { core::mem::ManuallyDrop::new(core::mem::replace(&mut stdio, Stdio::Ignore)); Readable::Memfd(fd) } - Stdio::Pipe => Readable::Pipe(PipeReader::create( - event_loop, process, result, None, out_type, interp, - )), - Stdio::ArrayBuffer(array_buffer) => { - let mut pipe = - PipeReader::create(event_loop, process, result, None, out_type, interp); - // The Arc was just created by `PipeReader::create` and is - // uniquely held (strong=1, weak=0) — `get_mut` is the safe - // route to set `buffered_output` before it's shared. - Arc::get_mut(&mut pipe) - .expect("fresh PipeReader Arc") - .buffered_output = BufferedOutput::ArrayBuffer { - buf: core::mem::take(array_buffer), - i: 0, - }; - Readable::Pipe(pipe) - } - Stdio::Capture(_) => Readable::Pipe(PipeReader::create( - event_loop, process, result, shellio, out_type, interp, - )), - Stdio::ReadableStream(_) => Readable::Ignore, // Shell doesn't use readable_stream } + Stdio::Pipe => Readable::Pipe(PipeReader::create( + event_loop, process, result, None, out_type, interp, + )), + Stdio::ArrayBuffer(array_buffer) => { + let mut pipe = + PipeReader::create(event_loop, process, result, None, out_type, interp); + // The Arc was just created by `PipeReader::create` and is + // uniquely held (strong=1, weak=0) — `get_mut` is the safe + // route to set `buffered_output` before it's shared. + Arc::get_mut(&mut pipe) + .expect("fresh PipeReader Arc") + .buffered_output = BufferedOutput::ArrayBuffer { + buf: core::mem::take(array_buffer), + i: 0, + }; + Readable::Pipe(pipe) + } + Stdio::Capture(_) => Readable::Pipe(PipeReader::create( + event_loop, process, result, shellio, out_type, interp, + )), + Stdio::ReadableStream(_) => Readable::Ignore, // Shell doesn't use readable_stream } } diff --git a/src/runtime/socket/socket_body.rs b/src/runtime/socket/socket_body.rs index e29bfb98a039..ad7f58441ae6 100644 --- a/src/runtime/socket/socket_body.rs +++ b/src/runtime/socket/socket_body.rs @@ -582,6 +582,32 @@ impl NewSocket { } } + /// Shared tail of the simple uws event callbacks (`on_writable`, + /// `on_timeout`, `on_end`, `on_data`): keeps `handlers` alive across the + /// user callback so the error handler can still be reached, routes a + /// thrown exception there, and nulls `self.handlers` when the scope's + /// exit frees the client `Handlers`. `extra_args` (at most one) follow + /// the implicit `this` argument. + #[inline] + fn call_socket_handler( + &self, + handlers: bun_ptr::BackRef, + callback: JSValue, + extra_args: &[JSValue], + ) { + let scope = Handlers::enter_ref(handlers); + let global = handlers.global_object; + let this_value = self.get_this_value(&global); + let mut args = [this_value; 2]; + args[1..1 + extra_args.len()].copy_from_slice(extra_args); + if let Err(err) = callback.call(&global, this_value, &args[..1 + extra_args.len()]) { + let _ = handlers.call_error_handler(this_value, &[this_value, global.take_error(err)]); + } + if scope.exit() { + self.handlers.set(None); + } + } + /// Noalias re-entrancy: takes `this: *mut Self`, NOT /// `&mut self`. `callback.call(...)` re-enters JS which can call /// `socket.write()`/`socket.end()`/`socket.reload()` on this same wrapper @@ -630,18 +656,7 @@ impl NewSocket { return; } - // the handlers must be kept alive for the duration of the function call - // that way if we need to call the error handler, we can - let scope = Handlers::enter_ref(handlers); - - let global = handlers.global_object; - let this_value = this.get_this_value(&global); - if let Err(err) = callback.call(&global, this_value, &[this_value]) { - let _ = handlers.call_error_handler(this_value, &[this_value, global.take_error(err)]); - } - if scope.exit() { - this.handlers.set(None); - } + this.call_socket_handler(handlers, callback, &[]); this.deref(); } @@ -673,18 +688,7 @@ impl NewSocket { return; } - // the handlers must be kept alive for the duration of the function call - // that way if we need to call the error handler, we can - let scope = Handlers::enter_ref(handlers); - - let global = handlers.global_object; - let this_value = this.get_this_value(&global); - if let Err(err) = callback.call(&global, this_value, &[this_value]) { - let _ = handlers.call_error_handler(this_value, &[this_value, global.take_error(err)]); - } - if scope.exit() { - this.handlers.set(None); - } + this.call_socket_handler(handlers, callback, &[]); } /// Returns the raw, freely-aliased @@ -1245,18 +1249,7 @@ impl NewSocket { return; } - // the handlers must be kept alive for the duration of the function call - // that way if we need to call the error handler, we can - let scope = Handlers::enter_ref(handlers); - - let global = handlers.global_object; - let this_value = this.get_this_value(&global); - if let Err(err) = callback.call(&global, this_value, &[this_value]) { - let _ = handlers.call_error_handler(this_value, &[this_value, global.take_error(err)]); - } - if scope.exit() { - this.handlers.set(None); - } + this.call_socket_handler(handlers, callback, &[]); this.deref(); } @@ -1572,7 +1565,6 @@ impl NewSocket { } let global = handlers.global_object; - let this_value = this.get_this_value(&global); let output_value = match handlers.binary_type.to_js(data, &global) { Ok(v) => v, Err(err) => { @@ -1581,17 +1573,7 @@ impl NewSocket { } }; - // the handlers must be kept alive for the duration of the function call - // that way if we need to call the error handler, we can - let scope = Handlers::enter_ref(handlers); - - // const encoding = handlers.encoding; - if let Err(err) = callback.call(&global, this_value, &[this_value, output_value]) { - let _ = handlers.call_error_handler(this_value, &[this_value, global.take_error(err)]); - } - if scope.exit() { - this.handlers.set(None); - } + this.call_socket_handler(handlers, callback, &[output_value]); } #[bun_jsc::host_fn(getter)] diff --git a/src/runtime/socket/udp_socket.rs b/src/runtime/socket/udp_socket.rs index 760d4fdaeb05..7e051fee2c65 100644 --- a/src/runtime/socket/udp_socket.rs +++ b/src/runtime/socket/udp_socket.rs @@ -7,8 +7,8 @@ use bun_jsc::JsCell; use bun_jsc::array_buffer::BinaryType; use bun_jsc::virtual_machine::VirtualMachine; use bun_jsc::{ - CallFrame, JSGlobalObject, JSValue, JsRef, JsResult, MarkedArgumentBuffer, Ref as JscRef, - StringJsc, SysErrorJsc, SystemError, + CallFrame, JSGlobalObject, JSValue, JsError, JsRef, JsResult, MarkedArgumentBuffer, + Ref as JscRef, StringJsc, SysErrorJsc, SystemError, }; use bun_ptr::BackRef; @@ -679,63 +679,38 @@ impl UDPSocket { event_loop.exit(); } - #[bun_jsc::host_fn(method)] - pub fn set_broadcast( - this: &Self, - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - if this.closed.get() { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) + fn throw_setsockopt_errno(global_this: &JSGlobalObject, errno: SystemErrno) -> JsError { + global_this.throw_value( + bun_sys::Error::from_code_int(errno as c_int, bun_sys::Tag::setsockopt) .to_js(global_this), - )); - } - - let arguments = callframe.arguments(); - if arguments.len() < 1 { - return Err(global_this.throw_invalid_arguments(format_args!( - "Expected 1 argument, got {}", - arguments.len() - ))); - } - - let enabled = arguments[0].to_boolean(); - let Some(socket) = this.socket.get() else { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), - )); - }; - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - let res = uws::udp::Socket::opaque_mut(socket).set_broadcast(enabled); + ) + } + fn check_setsockopt(global_this: &JSGlobalObject, res: c_int) -> JsResult<()> { if let Some(err) = get_us_error::(res, bun_sys::Tag::setsockopt) { return Err(global_this.throw_value(err.to_js(global_this))); } + Ok(()) + } - Ok(arguments[0]) + fn require_socket(&self, global_this: &JSGlobalObject) -> JsResult<&mut uws::udp::Socket> { + let Some(socket) = self.socket.get() else { + return Err(global_this.throw(format_args!("Socket is closed"))); + }; + // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. + Ok(uws::udp::Socket::opaque_mut(socket)) } - #[bun_jsc::host_fn(method)] - pub fn set_multicast_loopback( + fn set_bool_opt( this: &Self, global_this: &JSGlobalObject, callframe: &CallFrame, + function: fn(&mut uws::udp::Socket, bool) -> c_int, ) -> JsResult { if this.closed.get() { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EBADF, )); } @@ -753,24 +728,46 @@ impl UDPSocket { // test-dgram-multicast-loopback.js). Throw EBADF to match the // `closed` branch above instead of panicking. let Some(socket) = this.socket.get() else { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EBADF, )); }; // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - let res = uws::udp::Socket::opaque_mut(socket).set_multicast_loopback(enabled); - - if let Some(err) = get_us_error::(res, bun_sys::Tag::setsockopt) { - return Err(global_this.throw_value(err.to_js(global_this))); - } + let res = function(uws::udp::Socket::opaque_mut(socket), enabled); + Self::check_setsockopt(global_this, res)?; Ok(arguments[0]) } + #[bun_jsc::host_fn(method)] + pub fn set_broadcast( + this: &Self, + global_this: &JSGlobalObject, + callframe: &CallFrame, + ) -> JsResult { + Self::set_bool_opt( + this, + global_this, + callframe, + uws::udp::Socket::set_broadcast, + ) + } + + #[bun_jsc::host_fn(method)] + pub fn set_multicast_loopback( + this: &Self, + global_this: &JSGlobalObject, + callframe: &CallFrame, + ) -> JsResult { + Self::set_bool_opt( + this, + global_this, + callframe, + uws::udp::Socket::set_multicast_loopback, + ) + } + fn set_membership( this: &Self, global_this: &JSGlobalObject, @@ -778,12 +775,9 @@ impl UDPSocket { drop: bool, ) -> JsResult { if this.closed.get() { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EBADF, )); } @@ -802,20 +796,15 @@ impl UDPSocket { arguments[0], &mut addr, )? { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EINVAL as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EINVAL, )); } let mut interface: sockaddr_storage = bun_core::ffi::zeroed(); - let Some(socket) = this.socket.get() else { - return Err(global_this.throw(format_args!("Socket is closed"))); - }; + let socket = this.require_socket(global_this)?; let res = if arguments.len() > 1 && this.parse_addr( @@ -829,15 +818,12 @@ impl UDPSocket { "Family mismatch between address and interface" ))); } - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - uws::udp::Socket::opaque_mut(socket).set_membership(&addr, Some(&interface), drop) + socket.set_membership(&addr, Some(&interface), drop) } else { - uws::udp::Socket::opaque_mut(socket).set_membership(&addr, None, drop) + socket.set_membership(&addr, None, drop) }; - if let Some(err) = get_us_error::(res, bun_sys::Tag::setsockopt) { - return Err(global_this.throw_value(err.to_js(global_this))); - } + Self::check_setsockopt(global_this, res)?; Ok(JSValue::TRUE) } @@ -867,12 +853,9 @@ impl UDPSocket { drop: bool, ) -> JsResult { if this.closed.get() { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EBADF, )); } @@ -894,12 +877,9 @@ impl UDPSocket { arguments[0], &mut source_addr, )? { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EINVAL as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EINVAL, )); } @@ -910,12 +890,9 @@ impl UDPSocket { arguments[1], &mut group_addr, )? { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EINVAL as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EINVAL, )); } @@ -927,9 +904,7 @@ impl UDPSocket { let mut interface: sockaddr_storage = bun_core::ffi::zeroed(); - let Some(socket) = this.socket.get() else { - return Err(global_this.throw(format_args!("Socket is closed"))); - }; + let socket = this.require_socket(global_this)?; let res = if arguments.len() > 2 && this.parse_addr( @@ -943,25 +918,12 @@ impl UDPSocket { "Family mismatch among source, group and interface addresses" ))); } - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - uws::udp::Socket::opaque_mut(socket).set_source_specific_membership( - &source_addr, - &group_addr, - Some(&interface), - drop, - ) + socket.set_source_specific_membership(&source_addr, &group_addr, Some(&interface), drop) } else { - uws::udp::Socket::opaque_mut(socket).set_source_specific_membership( - &source_addr, - &group_addr, - None, - drop, - ) + socket.set_source_specific_membership(&source_addr, &group_addr, None, drop) }; - if let Some(err) = get_us_error::(res, bun_sys::Tag::setsockopt) { - return Err(global_this.throw_value(err.to_js(global_this))); - } + Self::check_setsockopt(global_this, res)?; Ok(JSValue::TRUE) } @@ -991,12 +953,9 @@ impl UDPSocket { callframe: &CallFrame, ) -> JsResult { if this.closed.get() { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EBADF, )); } @@ -1026,16 +985,9 @@ impl UDPSocket { return Ok(JSValue::FALSE); } - let Some(socket) = this.socket.get() else { - return Err(global_this.throw(format_args!("Socket is closed"))); - }; - - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - let res = uws::udp::Socket::opaque_mut(socket).set_multicast_interface(&addr); - - if let Some(err) = get_us_error::(res, bun_sys::Tag::setsockopt) { - return Err(global_this.throw_value(err.to_js(global_this))); - } + let socket = this.require_socket(global_this)?; + let res = socket.set_multicast_interface(&addr); + Self::check_setsockopt(global_this, res)?; Ok(JSValue::TRUE) } @@ -1075,12 +1027,9 @@ impl UDPSocket { function: fn(&mut uws::udp::Socket, i32) -> c_int, ) -> JsResult { if this.closed.get() { - return Err(global_this.throw_value( - bun_sys::Error::from_code_int( - SystemErrno::EBADF as c_int, - bun_sys::Tag::setsockopt, - ) - .to_js(global_this), + return Err(Self::throw_setsockopt_errno( + global_this, + SystemErrno::EBADF, )); } @@ -1093,15 +1042,9 @@ impl UDPSocket { } let ttl = arguments[0].coerce_to_i32(global_this)?; - let Some(socket) = this.socket.get() else { - return Err(global_this.throw(format_args!("Socket is closed"))); - }; - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - let res = function(uws::udp::Socket::opaque_mut(socket), ttl); - - if let Some(err) = get_us_error::(res, bun_sys::Tag::setsockopt) { - return Err(global_this.throw_value(err.to_js(global_this))); - } + let socket = this.require_socket(global_this)?; + let res = function(socket, ttl); + Self::check_setsockopt(global_this, res)?; Ok(JSValue::js_number(ttl as f64)) } diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index bdb88a682c81..f197a55b6134 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -3089,6 +3089,175 @@ pub mod mock { } } + /// Compares one `mock.calls` entry (a JSArray of call arguments) against + /// the expected arguments: length pre-check, then per-argument + /// `jest_deep_equals` with early exit on the first mismatch. + pub(crate) fn call_args_equal( + global: &JSGlobalObject, + call_item: JSValue, + expected: &[JSValue], + ) -> JsResult { + if call_item.get_length(global)? != expected.len() as u64 { + return Ok(false); + } + let mut itr = call_item.array_iterator(global)?; + while let Some(call_arg) = itr.next()? { + if !call_arg.jest_deep_equals(expected[itr.i as usize - 1], global)? { + return Ok(false); + } + } + Ok(true) + } + + /// A `mock.results` entry parsed into its `type` tag. + pub(crate) enum MockResult { + /// Carries the entry's `value`. + Return(JSValue), + /// Carries the result object itself; callers that need the thrown + /// `value` fetch it lazily (`toHaveReturnedWith` never reads it). + Throw(JSValue), + /// Not an object, no string `type`, or an unrecognized tag (e.g. + /// "incomplete") — the `*ReturnedWith` matchers skip these entries. + Other, + } + + pub(crate) fn parse_mock_result(global: &JSGlobalObject, result: JSValue) -> JsResult { + if result.is_object() { + let result_type = result.get(global, "type")?.unwrap_or(JSValue::UNDEFINED); + if result_type.is_string() { + let type_str = bun_core::OwnedString::new(result_type.to_bun_string(global)?); + if type_str.eql_comptime("return") { + return Ok(MockResult::Return(result.get(global, "value")?.unwrap_or(JSValue::UNDEFINED))); + } + if type_str.eql_comptime("throw") { + return Ok(MockResult::Throw(result)); + } + } + } + Ok(MockResult::Other) + } + + // ── shared failure epilogues for the `toHave*With` matcher family ────── + // Each matcher keeps only its index-selection logic and message verbs; + // the throw shapes below are byte-identical across the family. + + /// `.not` failure: `"\n\n{lead}: {expected}{tail}"` under the + /// `not`-form signature. + pub(crate) fn throw_not_failure( + this: &Expect, + global: &JSGlobalObject, + matcher_name: &'static str, + matcher_params: &'static str, + lead: fmt::Arguments<'_>, + expected: JSValue, + tail: &'static str, + ) -> JsResult { + let mut formatter = make_formatter(global); + this.throw( + global, + Expect::get_signature(matcher_name, matcher_params, true), + format_args!("\n\n{}: {}{}", lead, expected.to_fmt(&mut formatter), tail), + ) + } + + /// `"Expected: {expected}\nBut it was not called."` failure. + pub(crate) fn throw_not_called( + this: &Expect, + global: &JSGlobalObject, + signature: &'static str, + expected: JSValue, + ) -> JsResult { + let mut formatter = make_formatter(global); + this.throw( + global, + signature, + format_args!("\n\nExpected: {}\nBut it was not called.", expected.to_fmt(&mut formatter)), + ) + } + + /// `"called N time(s), but call M was requested"` failure (`*Nth*` matchers). + pub(crate) fn throw_nth_call_missing( + this: &Expect, + global: &JSGlobalObject, + signature: &'static str, + total_calls: u32, + n: u32, + tail: &'static str, + ) -> JsResult { + this.throw( + global, + signature, + format_args!( + "\n\nThe mock function was called {} time{}, but call {} was requested.{}", + total_calls, + if total_calls == 1 { "" } else { "s" }, + n, + tail, + ), + ) + } + + /// Diff failure: `"\n\n{prefix}{DiffFormatter}\n"`. + pub(crate) fn throw_diff( + this: &Expect, + global: &JSGlobalObject, + signature: &'static str, + prefix: fmt::Arguments<'_>, + expected: JSValue, + received: JSValue, + ) -> JsResult { + let diff_format = DiffFormatter { + expected: Some(expected), + received: Some(received), + expected_string: None, + received_string: None, + global_this: Some(global), + not: false, + }; + this.throw(global, signature, format_args!("\n\n{}{}\n", prefix, diff_format)) + } + + /// `"{prefix}Expected: X\nReceived: Y"` failure. Two formatters because the + /// `ZigFormatter` adapter holds `&mut Formatter`, so two live adapters + /// cannot alias the same backing formatter. + pub(crate) fn throw_expected_received( + this: &Expect, + global: &JSGlobalObject, + signature: &'static str, + prefix: fmt::Arguments<'_>, + expected: JSValue, + received: JSValue, + ) -> JsResult { + let mut f1 = make_formatter(global); + let mut f2 = make_formatter(global); + this.throw( + global, + signature, + format_args!( + "\n\n{}Expected: {}\nReceived: {}", + prefix, + expected.to_fmt(&mut f1), + received.to_fmt(&mut f2), + ), + ) + } + + /// `"{which} threw an error: …"` failure (`toHave{Last,Nth}ReturnedWith`). + pub(crate) fn throw_call_threw( + this: &Expect, + global: &JSGlobalObject, + signature: &'static str, + which: fmt::Arguments<'_>, + error: JSValue, + ) -> JsResult { + let mut formatter = make_formatter(global); + this.throw( + global, + signature, + format_args!("\n\n{} threw an error: {}\n", which, error.to_fmt(&mut formatter)), + ) + } + pub(crate) fn jest_mock_return_object_type(global_this: &JSGlobalObject, value: JSValue) -> JsResult { if let Some(type_string) = value.fast_get(global_this, bun_jsc::BuiltinName::Type)? { if type_string.is_string() { diff --git a/src/runtime/test_runner/expect/toEqual.rs b/src/runtime/test_runner/expect/toEqual.rs index 8cc083a3f7cb..61b80fba9a70 100644 --- a/src/runtime/test_runner/expect/toEqual.rs +++ b/src/runtime/test_runner/expect/toEqual.rs @@ -9,19 +9,40 @@ impl Expect { &self, global: &JSGlobalObject, frame: &CallFrame, + ) -> JsResult { + self.equals_impl(global, frame, "toEqual", JSValue::jest_deep_equals) + } + + #[bun_jsc::host_fn(method)] + pub fn to_strict_equal( + &self, + global: &JSGlobalObject, + frame: &CallFrame, + ) -> JsResult { + self.equals_impl(global, frame, "toStrictEqual", JSValue::jest_strict_deep_equals) + } + + fn equals_impl( + &self, + global: &JSGlobalObject, + frame: &CallFrame, + name: &'static str, + deep_equals: fn(JSValue, JSValue, &JSGlobalObject) -> JsResult, ) -> JsResult { let (this, value, not) = - self.matcher_prelude(global, frame.this(), "toEqual", "expected")?; + self.matcher_prelude(global, frame.this(), name, "expected")?; let _arguments = frame.arguments_old::<1>(); let arguments: &[JSValue] = _arguments.slice(); if arguments.len() < 1 { - return Err(global.throw_invalid_arguments(format_args!("toEqual() requires 1 argument"))); + return Err( + global.throw_invalid_arguments(format_args!("{name}() requires 1 argument")) + ); } let expected = arguments[0]; - let mut pass = value.jest_deep_equals(expected, global)?; + let mut pass = deep_equals(value, expected, global)?; if not { pass = !pass; @@ -40,12 +61,7 @@ impl Expect { not, }; - if not { - let signature: &str = Expect::get_signature("toEqual", "expected", true); - return this.throw(global, signature, format_args!("\n\n{}\n", diff_formatter)); - } - - let signature: &str = Expect::get_signature("toEqual", "expected", false); + let signature: &str = Expect::get_signature(name, "expected", not); this.throw(global, signature, format_args!("\n\n{}\n", diff_formatter)) } } diff --git a/src/runtime/test_runner/expect/toHaveBeenCalledWith.rs b/src/runtime/test_runner/expect/toHaveBeenCalledWith.rs index defb78cb26f5..f177dbdc7e03 100644 --- a/src/runtime/test_runner/expect/toHaveBeenCalledWith.rs +++ b/src/runtime/test_runner/expect/toHaveBeenCalledWith.rs @@ -1,6 +1,5 @@ use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; -use super::DiffFormatter; use super::mock; use super::Expect; @@ -32,20 +31,7 @@ pub(crate) fn to_have_been_called_with( ))); } - if call_item.get_length(global)? != arguments.len() as u64 { - continue; - } - - let mut call_itr = call_item.array_iterator(global)?; - let mut matched = true; - while let Some(call_arg) = call_itr.next()? { - if !call_arg.jest_deep_equals(arguments[call_itr.i as usize - 1], global)? { - matched = false; - break; - } - } - - if matched { + if mock::call_args_equal(global, call_item, arguments)? { pass = true; break; } @@ -57,52 +43,31 @@ pub(crate) fn to_have_been_called_with( } // handle failure - let mut formatter = super::make_formatter(global); - let expected_args_js_array = JSValue::create_array_from_slice(global, arguments)?; expected_args_js_array.ensure_still_alive(); if this.flags.get().not() { - let signature = Expect::get_signature("toHaveBeenCalledWith", "...expected", true); - return this.throw( - global, - signature, - format_args!( - "\n\nExpected mock function not to have been called with: {}\nBut it was.", - expected_args_js_array.to_fmt(&mut formatter), - ), + return mock::throw_not_failure( + &this, global, "toHaveBeenCalledWith", "...expected", + format_args!("Expected mock function not to have been called with"), expected_args_js_array, "\nBut it was.", ); } let signature = Expect::get_signature("toHaveBeenCalledWith", "...expected", false); if calls_count == 0 { - return this.throw( - global, - signature, - format_args!( - "\n\nExpected: {}\nBut it was not called.", - expected_args_js_array.to_fmt(&mut formatter), - ), - ); + return mock::throw_not_called(&this, global, signature, expected_args_js_array); } // If there's only one call, provide a nice diff. if calls_count == 1 { let received_call_args = calls.get_index(global, 0)?; - let diff_format = DiffFormatter { - received_string: None, - expected_string: None, - expected: Some(expected_args_js_array), - received: Some(received_call_args), - global_this: Some(global), - not: false, - }; - return this.throw(global, signature, format_args!("\n\n{}\n", diff_format)); + return mock::throw_diff(&this, global, signature, format_args!(""), expected_args_js_array, received_call_args); } // If there are multiple calls, list them all to help debugging. // The AllCallsWithArgsFormatter holds an exclusive borrow of the formatter, so // we allocate a second ConsoleObject formatter for the list. + let mut formatter = super::make_formatter(global); let mut list_fmt = super::make_formatter(global); let list_formatter = mock::AllCallsWithArgsFormatter { global_this: global, diff --git a/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.rs b/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.rs index 735c24befba5..0142baba9a48 100644 --- a/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.rs +++ b/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.rs @@ -1,6 +1,6 @@ use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; -use super::DiffFormatter; +use super::mock; use super::{Expect, get_signature}; pub(crate) fn to_have_been_last_called_with( @@ -15,7 +15,7 @@ pub(crate) fn to_have_been_last_called_with( frame.this(), "toHaveBeenLastCalledWith", "...expected", - super::mock::MockKind::CallsWithSig, + mock::MockKind::CallsWithSig, )?; let total_calls: u32 = calls.get_length(global)? as u32; @@ -34,17 +34,7 @@ pub(crate) fn to_have_been_last_called_with( ))); } - if last_call_value.get_length(global)? != arguments.len() as u64 { - pass = false; - } else { - let mut itr = last_call_value.array_iterator(global)?; - while let Some(call_arg) = itr.next()? { - if !call_arg.jest_deep_equals(arguments[itr.i as usize - 1], global)? { - pass = false; - break; - } - } - } + pass = mock::call_args_equal(global, last_call_value, arguments)?; } if pass != this.flags.get().not() { @@ -52,42 +42,20 @@ pub(crate) fn to_have_been_last_called_with( } // handle failure - let mut formatter = super::make_formatter(global); - let expected_args_js_array = JSValue::create_array_from_slice(global, arguments)?; expected_args_js_array.ensure_still_alive(); if this.flags.get().not() { - let signature = get_signature("toHaveBeenLastCalledWith", "...expected", true); - return this.throw( - global, - signature, - format_args!( - "\n\nExpected last call not to be with: {}\nBut it was.", - expected_args_js_array.to_fmt(&mut formatter), - ), + return mock::throw_not_failure( + &this, global, "toHaveBeenLastCalledWith", "...expected", + format_args!("Expected last call not to be with"), expected_args_js_array, "\nBut it was.", ); } let signature = get_signature("toHaveBeenLastCalledWith", "...expected", false); if total_calls == 0 { - return this.throw( - global, - signature, - format_args!( - "\n\nExpected: {}\nBut it was not called.", - expected_args_js_array.to_fmt(&mut formatter), - ), - ); + return mock::throw_not_called(&this, global, signature, expected_args_js_array); } - let diff_format = DiffFormatter { - expected: Some(expected_args_js_array), - received: Some(last_call_value), - expected_string: None, - received_string: None, - global_this: Some(global), - not: false, - }; - this.throw(global, signature, format_args!("\n\n{}\n", diff_format)) + mock::throw_diff(&this, global, signature, format_args!(""), expected_args_js_array, last_call_value) } diff --git a/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.rs b/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.rs index fe270de99f3e..691aeac36fec 100644 --- a/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.rs +++ b/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.rs @@ -1,5 +1,5 @@ use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; -use super::DiffFormatter; +use super::mock; use super::Expect; pub(crate) fn to_have_been_nth_called_with( @@ -13,7 +13,7 @@ pub(crate) fn to_have_been_nth_called_with( frame.this(), "toHaveBeenNthCalledWith", "n, ...expected", - super::mock::MockKind::CallsWithSig, + mock::MockKind::CallsWithSig, )?; if arguments.is_empty() || !arguments[0].is_any_int() { @@ -36,7 +36,6 @@ pub(crate) fn to_have_been_nth_called_with( if pass { nth_call_value = calls.get_index(global, nth_call_num - 1)?; - let expected_args = &arguments[1..]; if !nth_call_value.js_type().is_array() { return Err(global.throw(format_args!( @@ -44,17 +43,7 @@ pub(crate) fn to_have_been_nth_called_with( ))); } - if nth_call_value.get_length(global)? != expected_args.len() as u64 { - pass = false; - } else { - let mut itr = nth_call_value.array_iterator(global)?; - while let Some(call_arg) = itr.next()? { - if !call_arg.jest_deep_equals(expected_args[(itr.i - 1) as usize], global)? { - pass = false; - break; - } - } - } + pass = mock::call_args_equal(global, nth_call_value, &arguments[1..])?; } if pass != this.flags.get().not() { @@ -62,52 +51,25 @@ pub(crate) fn to_have_been_nth_called_with( } // handle failure - let mut formatter = super::make_formatter(global); - - let expected_args_slice = &arguments[1..]; - let expected_args_js_array = JSValue::create_array_from_slice(global, expected_args_slice)?; + let expected_args_js_array = JSValue::create_array_from_slice(global, &arguments[1..])?; expected_args_js_array.ensure_still_alive(); if this.flags.get().not() { - let signature = Expect::get_signature("toHaveBeenNthCalledWith", "n, ...expected", true); - return this.throw( - global, - signature, - format_args!( - "\n\nExpected call #{} not to be with: {}\nBut it was.", - nth_call_num, - expected_args_js_array.to_fmt(&mut formatter), - ), + return mock::throw_not_failure( + &this, global, "toHaveBeenNthCalledWith", "n, ...expected", + format_args!("Expected call #{} not to be with", nth_call_num), expected_args_js_array, "\nBut it was.", ); } let signature = Expect::get_signature("toHaveBeenNthCalledWith", "n, ...expected", false); // Handle case where function was not called enough times if total_calls < nth_call_num { - return this.throw( - global, - signature, - format_args!( - "\n\nThe mock function was called {} time{}, but call {} was requested.", - total_calls, - if total_calls == 1 { "" } else { "s" }, - nth_call_num, - ), - ); + return mock::throw_nth_call_missing(&this, global, signature, total_calls, nth_call_num, ""); } // The call existed but didn't match. Show a diff. - let diff_format = DiffFormatter { - expected: Some(expected_args_js_array), - received: Some(nth_call_value), - expected_string: None, - received_string: None, - global_this: Some(global), - not: false, - }; - this.throw( - global, - signature, - format_args!("\n\nCall #{}:\n{}\n", nth_call_num, diff_format), + mock::throw_diff( + &this, global, signature, + format_args!("Call #{}:\n", nth_call_num), expected_args_js_array, nth_call_value, ) } diff --git a/src/runtime/test_runner/expect/toHaveLastReturnedWith.rs b/src/runtime/test_runner/expect/toHaveLastReturnedWith.rs index 750bd4b81eee..41e3df359b8d 100644 --- a/src/runtime/test_runner/expect/toHaveLastReturnedWith.rs +++ b/src/runtime/test_runner/expect/toHaveLastReturnedWith.rs @@ -1,8 +1,6 @@ use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; -use super::FormatterTestExt; -use bun_jsc::console_object::Formatter; -use super::DiffFormatter; +use super::mock; use super::Expect; pub(crate) fn to_have_last_returned_with( @@ -17,7 +15,7 @@ pub(crate) fn to_have_last_returned_with( callframe.this(), "toHaveBeenLastReturnedWith", "expected", - super::mock::MockKind::Returns, + mock::MockKind::Returns, )?; let calls_count = u32::try_from(returns.get_length(global_this)?).unwrap(); @@ -29,24 +27,18 @@ pub(crate) fn to_have_last_returned_with( if calls_count > 0 { let last_result = returns.get_direct_index(global_this, calls_count - 1); - if last_result.is_object() { - let result_type = last_result.get(global_this, "type")?.unwrap_or(JSValue::UNDEFINED); - if result_type.is_string() { - let type_str = bun_core::OwnedString::new(result_type.to_bun_string(global_this)?); - - if type_str.eql_comptime("return") { - last_return_value = - last_result.get(global_this, "value")?.unwrap_or(JSValue::UNDEFINED); - - if last_return_value.jest_deep_equals(expected, global_this)? { - pass = true; - } - } else if type_str.eql_comptime("throw") { - last_call_threw = true; - last_error_value = - last_result.get(global_this, "value")?.unwrap_or(JSValue::UNDEFINED); + match mock::parse_mock_result(global_this, last_result)? { + mock::MockResult::Return(value) => { + last_return_value = value; + if last_return_value.jest_deep_equals(expected, global_this)? { + pass = true; } } + mock::MockResult::Throw(result) => { + last_call_threw = true; + last_error_value = result.get(global_this, "value")?.unwrap_or(JSValue::UNDEFINED); + } + mock::MockResult::Other => {} } } @@ -55,68 +47,27 @@ pub(crate) fn to_have_last_returned_with( } // Handle failure - let mut formatter = Formatter::new(global_this).with_quote_strings(true); - let signature = Expect::get_signature("toHaveBeenLastReturnedWith", "expected", false); if this.flags.get().not() { - return this.throw( - global_this, - Expect::get_signature("toHaveBeenLastReturnedWith", "expected", true), - format_args!( - concat!( - "\n\n", - "Expected mock function not to have last returned: {}\n", - "But it did.\n", - ), - expected.to_fmt(&mut formatter), - ), + return mock::throw_not_failure( + &this, global_this, "toHaveBeenLastReturnedWith", "expected", + format_args!("Expected mock function not to have last returned"), expected, "\nBut it did.\n", ); } if calls_count == 0 { - return this.throw( - global_this, - signature, - format_args!(concat!("\n\n", "The mock function was not called.")), - ); + return this.throw(global_this, signature, format_args!("\n\nThe mock function was not called.")); } if last_call_threw { - return this.throw( - global_this, - signature, - format_args!( - concat!("\n\n", "The last call threw an error: {}\n"), - last_error_value.to_fmt(&mut formatter), - ), - ); + return mock::throw_call_threw(&this, global_this, signature, format_args!("The last call"), last_error_value); } // Diff if possible if expected.is_string() && last_return_value.is_string() { - let diff_format = DiffFormatter { - received_string: None, - expected_string: None, - expected: Some(expected), - received: Some(last_return_value), - global_this: Some(global_this), - not: false, - }; - return this.throw(global_this, signature, format_args!("\n\n{}\n", diff_format)); + return mock::throw_diff(&this, global_this, signature, format_args!(""), expected, last_return_value); } - // The `ZigFormatter` adapter holds `&'a mut Formatter`, so two live adapters cannot alias - // the same backing formatter. Use a second formatter for the received value — - // `make_formatter` is a trivial struct init with no shared state between values. - let mut formatter2 = super::make_formatter(global_this); - this.throw( - global_this, - signature, - format_args!( - "\n\nExpected: {}\nReceived: {}", - expected.to_fmt(&mut formatter), - last_return_value.to_fmt(&mut formatter2), - ), - ) + mock::throw_expected_received(&this, global_this, signature, format_args!(""), expected, last_return_value) } diff --git a/src/runtime/test_runner/expect/toHaveNthReturnedWith.rs b/src/runtime/test_runner/expect/toHaveNthReturnedWith.rs index 8367a6e9c857..20fc77ef931a 100644 --- a/src/runtime/test_runner/expect/toHaveNthReturnedWith.rs +++ b/src/runtime/test_runner/expect/toHaveNthReturnedWith.rs @@ -1,6 +1,6 @@ use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; -use super::DiffFormatter; +use super::mock; use super::{Expect, get_signature}; pub(crate) fn to_have_nth_returned_with( @@ -15,7 +15,7 @@ pub(crate) fn to_have_nth_returned_with( frame.this(), "toHaveNthReturnedWith", "n, expected", - super::mock::MockKind::Returns, + mock::MockKind::Returns, )?; // Validate n is a number @@ -38,25 +38,22 @@ pub(crate) fn to_have_nth_returned_with( let mut nth_return_value: JSValue = JSValue::UNDEFINED; let mut nth_call_threw = false; let mut nth_error_value: JSValue = JSValue::UNDEFINED; - let mut nth_call_exists = false; + let nth_call_exists = index < calls_count; - if index < calls_count { - nth_call_exists = true; + if nth_call_exists { let nth_result = returns.get_direct_index(global, index); - if nth_result.is_object() { - let result_type = nth_result.get(global, "type")?.unwrap_or(JSValue::UNDEFINED); - if result_type.is_string() { - let type_str = bun_core::OwnedString::new(result_type.to_bun_string(global)?); - if type_str.eql_comptime("return") { - nth_return_value = nth_result.get(global, "value")?.unwrap_or(JSValue::UNDEFINED); - if nth_return_value.jest_deep_equals(expected, global)? { - pass = true; - } - } else if type_str.eql_comptime("throw") { - nth_call_threw = true; - nth_error_value = nth_result.get(global, "value")?.unwrap_or(JSValue::UNDEFINED); + match mock::parse_mock_result(global, nth_result)? { + mock::MockResult::Return(value) => { + nth_return_value = value; + if nth_return_value.jest_deep_equals(expected, global)? { + pass = true; } } + mock::MockResult::Throw(result) => { + nth_call_threw = true; + nth_error_value = result.get(global, "value")?.unwrap_or(JSValue::UNDEFINED); + } + mock::MockResult::Other => {} } } @@ -65,74 +62,27 @@ pub(crate) fn to_have_nth_returned_with( } // Handle failure - let mut formatter = super::make_formatter(global); - let mut formatter2 = super::make_formatter(global); - // defer formatter.deinit() — handled by Drop - let signature = get_signature("toHaveNthReturnedWith", "n, expected", false); if this.flags.get().not() { - return this.throw( - global, - get_signature("toHaveNthReturnedWith", "n, expected", true), - format_args!( - "\n\nExpected mock function not to have returned on call {}: {}\nBut it did.\n", - n, - expected.to_fmt(&mut formatter), - ), + return mock::throw_not_failure( + &this, global, "toHaveNthReturnedWith", "n, expected", + format_args!("Expected mock function not to have returned on call {}", n), expected, "\nBut it did.\n", ); } if !nth_call_exists { - return this.throw( - global, - signature, - format_args!( - "\n\nThe mock function was called {} time{}, but call {} was requested.\n", - calls_count, - if calls_count == 1 { "" } else { "s" }, - n, - ), - ); + return mock::throw_nth_call_missing(&this, global, signature, calls_count, index + 1, "\n"); } if nth_call_threw { - return this.throw( - global, - signature, - format_args!( - "\n\nCall {} threw an error: {}\n", - n, - nth_error_value.to_fmt(&mut formatter), - ), - ); + return mock::throw_call_threw(&this, global, signature, format_args!("Call {}", n), nth_error_value); } // Diff if possible if expected.is_string() && nth_return_value.is_string() { - let diff_format = DiffFormatter { - expected: Some(expected), - received: Some(nth_return_value), - expected_string: None, - received_string: None, - global_this: Some(global), - not: false, - }; - return this.throw( - global, - signature, - format_args!("\n\nCall {}:\n{}\n", n, diff_format), - ); + return mock::throw_diff(&this, global, signature, format_args!("Call {}:\n", n), expected, nth_return_value); } - this.throw( - global, - signature, - format_args!( - "\n\nCall {}:\nExpected: {}\nReceived: {}", - n, - expected.to_fmt(&mut formatter), - nth_return_value.to_fmt(&mut formatter2), - ), - ) + mock::throw_expected_received(&this, global, signature, format_args!("Call {}:\n", n), expected, nth_return_value) } diff --git a/src/runtime/test_runner/expect/toHaveReturnedWith.rs b/src/runtime/test_runner/expect/toHaveReturnedWith.rs index d6bcd0365839..d293fca74d4e 100644 --- a/src/runtime/test_runner/expect/toHaveReturnedWith.rs +++ b/src/runtime/test_runner/expect/toHaveReturnedWith.rs @@ -1,6 +1,5 @@ use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; -use super::DiffFormatter; use super::mock; use super::Expect; @@ -32,25 +31,17 @@ pub(crate) fn to_have_returned_with( for i in 0..calls_count { let result = returns.get_direct_index(global, i); - if result.is_object() { - let result_type = result.get(global, "type")?.unwrap_or(JSValue::UNDEFINED); - if result_type.is_string() { - let type_str = bun_core::OwnedString::new(result_type.to_bun_string(global)?); - - if type_str.eql_comptime("return") { - let result_value = result.get(global, "value")?.unwrap_or(JSValue::UNDEFINED); - successful_returns.push(result_value); - - // Check for pass condition only if not already passed - if !pass { - if result_value.jest_deep_equals(expected, global)? { - pass = true; - } - } - } else if type_str.eql_comptime("throw") { - has_errors = true; + match mock::parse_mock_result(global, result)? { + mock::MockResult::Return(result_value) => { + successful_returns.push(result_value); + + // Check for pass condition only if not already passed + if !pass && result_value.jest_deep_equals(expected, global)? { + pass = true; } } + mock::MockResult::Throw(_) => has_errors = true, + mock::MockResult::Other => {} } } @@ -59,19 +50,12 @@ pub(crate) fn to_have_returned_with( } // Handle failure - let mut formatter = super::make_formatter(global); - let signature: &str = Expect::get_signature("toHaveReturnedWith", "expected", false); if this.flags.get().not() { - let not_signature: &str = Expect::get_signature("toHaveReturnedWith", "expected", true); - return this.throw( - global, - not_signature, - format_args!( - "\n\nExpected mock function not to have returned: {}\n", - expected.to_fmt(&mut formatter), - ), + return mock::throw_not_failure( + &this, global, "toHaveReturnedWith", "expected", + format_args!("Expected mock function not to have returned"), expected, "\n", ); } @@ -82,34 +66,15 @@ pub(crate) fn to_have_returned_with( if calls_count == 1 && successful_returns_count == 1 { let received = successful_returns[0]; if expected.is_string() && received.is_string() { - let diff_format = DiffFormatter { - expected: Some(expected), - received: Some(received), - expected_string: None, - received_string: None, - global_this: Some(global), - not: false, - }; - return this.throw(global, signature, format_args!("\n\n{}\n", diff_format)); + return mock::throw_diff(&this, global, signature, format_args!(""), expected, received); } - // The `ZigFormatter` adapter holds `&'a mut Formatter`, so two live adapters cannot alias - // the same backing formatter. Use a second formatter for the received value — - // `make_formatter` is a trivial struct init with no shared state between values. - let mut formatter2 = super::make_formatter(global); - return this.throw( - global, - signature, - format_args!( - "\n\nExpected: {}\nReceived: {}", - expected.to_fmt(&mut formatter), - received.to_fmt(&mut formatter2), - ), - ); + return mock::throw_expected_received(&this, global, signature, format_args!(""), expected, received); } // list_formatter holds &mut Formatter via RefCell, so a separate formatter is // required for the inline `expected.to_fmt` argument used alongside it in the same format_args!. + let mut formatter = super::make_formatter(global); let mut list_fmt = super::make_formatter(global); if has_errors { @@ -119,7 +84,7 @@ pub(crate) fn to_have_returned_with( returns, formatter: core::cell::RefCell::new(&mut list_fmt), }; - return this.throw( + this.throw( global, signature, format_args!( @@ -129,7 +94,7 @@ pub(crate) fn to_have_returned_with( successful_returns_count, calls_count, ), - ); + ) } else { // Case: No errors, but no match (and multiple returns) let list_formatter = mock::SuccessfulReturnsFormatter { @@ -137,7 +102,7 @@ pub(crate) fn to_have_returned_with( successful_returns: &successful_returns, formatter: core::cell::RefCell::new(&mut list_fmt), }; - return this.throw( + this.throw( global, signature, format_args!( @@ -146,6 +111,6 @@ pub(crate) fn to_have_returned_with( list_formatter, successful_returns_count, ), - ); + ) } } diff --git a/src/runtime/test_runner/expect/toStrictEqual.rs b/src/runtime/test_runner/expect/toStrictEqual.rs deleted file mode 100644 index 9c246be10229..000000000000 --- a/src/runtime/test_runner/expect/toStrictEqual.rs +++ /dev/null @@ -1,53 +0,0 @@ -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; - -use super::DiffFormatter; -use super::Expect; - -impl Expect { - #[bun_jsc::host_fn(method)] - pub fn to_strict_equal( - &self, - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - let (this, value, not) = - self.matcher_prelude(global, frame.this(), "toStrictEqual", "expected")?; - - let _arguments = frame.arguments_old::<1>(); - let arguments: &[JSValue] = _arguments.slice(); - - if arguments.len() < 1 { - return Err(global.throw_invalid_arguments( - format_args!("toStrictEqual() requires 1 argument"), - )); - } - - let expected = arguments[0]; - let mut pass = value.jest_strict_deep_equals(expected, global)?; - - if not { - pass = !pass; - } - if pass { - return Ok(JSValue::UNDEFINED); - } - - // handle failure - let diff_formatter = DiffFormatter { - received: Some(value), - expected: Some(expected), - received_string: None, - expected_string: None, - global_this: Some(global), - not, - }; - - if not { - let signature = Expect::get_signature("toStrictEqual", "expected", true); - return this.throw(global, signature, format_args!("\n\n{}\n", diff_formatter)); - } - - let signature = Expect::get_signature("toStrictEqual", "expected", false); - this.throw(global, signature, format_args!("\n\n{}\n", diff_formatter)) - } -} diff --git a/src/runtime/test_runner/mod.rs b/src/runtime/test_runner/mod.rs index 5b16e24b8573..986af8bf178a 100644 --- a/src/runtime/test_runner/mod.rs +++ b/src/runtime/test_runner/mod.rs @@ -577,7 +577,6 @@ pub mod expect { "toMatchSnapshot.rs" => to_match_snapshot, "toSatisfy.rs" => to_satisfy, "toStartWith.rs" => to_start_with, - "toStrictEqual.rs" => to_strict_equal, "toThrow.rs" => to_throw, "toThrowErrorMatchingInlineSnapshot.rs" => to_throw_error_matching_inline_snapshot, "toThrowErrorMatchingSnapshot.rs" => to_throw_error_matching_snapshot, diff --git a/src/runtime/test_runner/pretty_format.rs b/src/runtime/test_runner/pretty_format.rs index 2f1dd496b23f..c3d03d761539 100644 --- a/src/runtime/test_runner/pretty_format.rs +++ b/src/runtime/test_runner/pretty_format.rs @@ -1,4 +1,4 @@ -use core::cell::{Cell, RefCell}; +use core::cell::RefCell; use crate::test_runner::expect::JSValueTestExt; use core::ffi::c_void; @@ -126,25 +126,6 @@ pub enum MessageLevel { Info = 4, } -#[repr(u32)] -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum MessageType { - Log = 0, - Dir = 1, - DirXML = 2, - Table = 3, - Trace = 4, - StartGroup = 5, - StartGroupCollapsed = 6, - EndGroup = 7, - Clear = 8, - Assert = 9, - Timing = 10, - Profile = 11, - ProfileEnd = 12, - Image = 13, -} - #[derive(Copy, Clone, Default)] pub struct FormatOptions { pub enable_colors: bool, @@ -400,61 +381,6 @@ impl Drop for Formatter<'_> { } } -/// `Display` adapter for formatting a single [`JSValue`]. -/// -/// `Display::fmt` only gives us `&self`, so the -/// mutable handle is parked behind a `Cell` and moved out for the duration of -/// the call — this preserves unique-borrow provenance without the -/// `&shared → *const → *mut` cast that would be UB under Stacked Borrows. -pub struct ZigFormatter<'a, 'b> { - pub formatter: Cell>>, - pub global: &'b JSGlobalObject, - pub value: JSValue, -} - -impl<'a, 'b> ZigFormatter<'a, 'b> { - pub fn new(formatter: &'a mut Formatter<'b>, global: &'b JSGlobalObject, value: JSValue) -> Self { - Self { formatter: Cell::new(Some(formatter)), global, value } - } -} - -#[derive(thiserror::Error, Debug, strum::IntoStaticStr)] -pub enum WriteError { - #[error("UhOh")] - UhOh, -} - -impl core::fmt::Display for ZigFormatter<'_, '_> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - // Move the unique `&mut Formatter` out of the cell for the body; - // re-seat it (and clear `remaining_values`) on the way out so the - // adapter stays reusable. - let formatter: &mut Formatter<'_> = self - .formatter - .take() - .expect("ZigFormatter::fmt re-entered or used after consumption"); - - // Assigning a stack-local slice into `Formatter<'b>` would require `'b: 'local`, - // which borrowck rejects. The single-value path never reads `remaining_values` - // (only `StringPossiblyFormatted` consumes it, and `ZigFormatter` always emits a - // single tag), so leaving it `&[]` is observationally equivalent. - formatter.remaining_values = &[]; - formatter.global_this = self.global; - - let result = (|| { - let tag = Tag::get(self.value, self.global).map_err(|_| core::fmt::Error)?; - let mut adapter = bun_io::FmtAdapter::new(f); - formatter - .format::<_, false>(tag, &mut adapter, self.value, self.global) - .map_err(|_| core::fmt::Error) - })(); - - formatter.remaining_values = &[]; - self.formatter.set(Some(formatter)); - result - } -} - #[repr(u8)] #[derive(Copy, Clone, PartialEq, Eq, core::marker::ConstParamTy)] pub enum Tag { @@ -620,20 +546,7 @@ impl Tag { | JSType::ModuleNamespaceObject | JSType::GlobalObject => Tag::Object, - JSType::ArrayBuffer - | JSType::Int8Array - | JSType::Uint8Array - | JSType::Uint8ClampedArray - | JSType::Int16Array - | JSType::Uint16Array - | JSType::Int32Array - | JSType::Uint32Array - | JSType::Float16Array - | JSType::Float32Array - | JSType::Float64Array - | JSType::BigInt64Array - | JSType::BigUint64Array - | JSType::DataView => Tag::TypedArray, + t if t.is_array_buffer_like() => Tag::TypedArray, JSType::HeapBigInt => Tag::BigInt, @@ -2866,7 +2779,7 @@ impl AsymmetricMatcherFormatter for bun_jsc::console_object::Formatter<'_> { ) -> JsResult<()> { let global = self.global_this; self.format::( - bun_jsc::console_object::formatter::TagResult { tag: tag.into(), cell }, + bun_jsc::console_object::formatter::TagResult { tag, cell, custom: None }, w, v, global, diff --git a/src/runtime/valkey_jsc/js_valkey_functions.rs b/src/runtime/valkey_jsc/js_valkey_functions.rs index 5789403e5d6f..34c04abaac5b 100644 --- a/src/runtime/valkey_jsc/js_valkey_functions.rs +++ b/src/runtime/valkey_jsc/js_valkey_functions.rs @@ -102,7 +102,7 @@ fn promise_to_js(p: *mut JSPromise) -> JSValue { /// `this.send()` it, and convert the result to a `JsResult` — /// `Ok(promise.toJS())` on success, a JS-side Redis error value on failure. /// -/// All 7 `cmd_*!` macros and ~24 hand-written methods (`get`, `getBuffer`, +/// Both `cmd_*!` macros and ~24 hand-written methods (`get`, `getBuffer`, /// `set`, `incr`, `decr`, `exists`, `expire`, `ttl`, `srem`, `sadd`, /// `sismember`, `hmget`, `hincrby`, `hset`, `smove`, `publish`, /// `send_unsubscribe_request_and_cleanup`, …) duplicated this 15-line block @@ -162,15 +162,18 @@ pub(crate) mod compile { // Note: each command-shape generator is a `macro_rules!` that emits a // `#[bun_jsc::host_fn(method)]` inside the `impl JSValkeyClient` block: -// cmd_noargs! (), cmd_key! (key: RedisKey), -// cmd_key_varargs! (key: RedisKey, ...args: RedisKey[]), -// cmd_key_value! (key: RedisKey, value: RedisValue), -// cmd_key_value_value2! (key: RedisKey, value: RedisValue, value2: RedisValue), -// cmd_strings_varargs! (...strings: string[]), -// cmd_key_value_varargs! (key: RedisKey, value: RedisValue, ...args: RedisValue) - -macro_rules! cmd_noargs { - ($fn_name:ident, $name:literal, $command:literal, $state:ident) => { +// +// - cmd! extracts one positional argument per name, in order: +// cmd!(f, name, "CMD", state) () +// cmd!(f, name, "CMD", "key", state) (key: RedisKey) +// cmd!(f, name, "CMD", "key", "value", state) (key: RedisKey, value: RedisValue) +// - cmd_varargs! forwards every provided argument: `skip_null` silently drops +// undefined/null arguments, `strict` throws on them, and `required "arg"` +// additionally throws when the first argument is missing (implies +// skip_null). + +macro_rules! cmd { + ($fn_name:ident, $name:literal, $command:literal, $($argname:literal,)* $state:ident $(,)?) => { #[bun_jsc::host_fn(method)] pub fn $fn_name( this: &Self, @@ -180,84 +183,23 @@ macro_rules! cmd_noargs { compile::test_correct_state::<{ compile::ClientStateRequirement::$state }>( this, $name, )?; - send_cmd( - this, - global, - frame.this(), - $command.as_bytes(), - CommandArgs::Args(&[]), - CommandMeta::default(), - concat!("Failed to send ", $command), - ) - } - }; -} - -macro_rules! cmd_key { - ($fn_name:ident, $name:literal, $command:literal, $arg0_name:literal, $state:ident) => { - #[bun_jsc::host_fn(method)] - pub fn $fn_name( - this: &Self, - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - compile::test_correct_state::<{ compile::ClientStateRequirement::$state }>( - this, $name, - )?; - - let Some(key) = from_js(global, frame.argument(0))? else { - return Err(global.throw_invalid_argument_type( - bname($name), - $arg0_name, - "string or buffer", - )); - }; - send_cmd( - this, - global, - frame.this(), - $command.as_bytes(), - CommandArgs::Args(&[key]), - CommandMeta::default(), - concat!("Failed to send ", $command), - ) - } - }; -} - -macro_rules! cmd_key_varargs { - ($fn_name:ident, $name:literal, $command:literal, $arg0_name:literal, $state:ident) => { - #[bun_jsc::host_fn(method)] - pub fn $fn_name( - this: &Self, - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - compile::test_correct_state::<{ compile::ClientStateRequirement::$state }>( - this, $name, - )?; - - if frame.argument(0).is_undefined_or_null() { - return Err(global.throw_missing_arguments_value(&[$arg0_name])); - } - - let arguments = frame.arguments(); - let mut args: Vec = Vec::with_capacity(arguments.len()); - for arg in arguments { - if arg.is_undefined_or_null() { - continue; - } - - let Some(another) = from_js(global, *arg)? else { - return Err(global.throw_invalid_argument_type( - bname($name), - "additional arguments", - "string or buffer", - )); - }; - args.push(another); - } + #[allow(unused_mut)] + let mut arg_index = 0; + let args = [$( + { + let Some(arg) = from_js(global, frame.argument(arg_index))? else { + return Err(global.throw_invalid_argument_type( + bname($name), + $argname, + "string or buffer", + )); + }; + arg_index += 1; + arg + }, + )*]; + let _ = arg_index; send_cmd( this, global, @@ -271,130 +213,17 @@ macro_rules! cmd_key_varargs { }; } -macro_rules! cmd_key_value { - ($fn_name:ident, $name:literal, $command:literal, $arg0_name:literal, $arg1_name:literal, $state:ident) => { - #[bun_jsc::host_fn(method)] - pub fn $fn_name( - this: &Self, - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - compile::test_correct_state::<{ compile::ClientStateRequirement::$state }>( - this, $name, - )?; - - let Some(key) = from_js(global, frame.argument(0))? else { - return Err(global.throw_invalid_argument_type( - bname($name), - $arg0_name, - "string or buffer", - )); - }; - let Some(value) = from_js(global, frame.argument(1))? else { - return Err(global.throw_invalid_argument_type( - bname($name), - $arg1_name, - "string or buffer", - )); - }; - send_cmd( - this, - global, - frame.this(), - $command.as_bytes(), - CommandArgs::Args(&[key, value]), - CommandMeta::default(), - concat!("Failed to send ", $command), - ) - } +macro_rules! cmd_varargs { + ($fn_name:ident, $name:literal, $command:literal, required $arg0_name:literal, $state:ident $(,)?) => { + cmd_varargs!(@impl $fn_name, $name, $command, true, $state, $arg0_name); }; -} - -macro_rules! cmd_key_value_value2 { - ($fn_name:ident, $name:literal, $command:literal, $arg0_name:literal, $arg1_name:literal, $arg2_name:literal, $state:ident) => { - #[bun_jsc::host_fn(method)] - pub fn $fn_name( - this: &Self, - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - compile::test_correct_state::<{ compile::ClientStateRequirement::$state }>( - this, $name, - )?; - - let Some(key) = from_js(global, frame.argument(0))? else { - return Err(global.throw_invalid_argument_type( - bname($name), - $arg0_name, - "string or buffer", - )); - }; - let Some(value) = from_js(global, frame.argument(1))? else { - return Err(global.throw_invalid_argument_type( - bname($name), - $arg1_name, - "string or buffer", - )); - }; - let Some(value2) = from_js(global, frame.argument(2))? else { - return Err(global.throw_invalid_argument_type( - bname($name), - $arg2_name, - "string or buffer", - )); - }; - send_cmd( - this, - global, - frame.this(), - $command.as_bytes(), - CommandArgs::Args(&[key, value, value2]), - CommandMeta::default(), - concat!("Failed to send ", $command), - ) - } + ($fn_name:ident, $name:literal, $command:literal, skip_null, $state:ident $(,)?) => { + cmd_varargs!(@impl $fn_name, $name, $command, true, $state); }; -} - -macro_rules! cmd_strings_varargs { - ($fn_name:ident, $name:literal, $command:literal, $state:ident) => { - #[bun_jsc::host_fn(method)] - pub fn $fn_name( - this: &Self, - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - compile::test_correct_state::<{ compile::ClientStateRequirement::$state }>( - this, $name, - )?; - - let mut args: Vec = Vec::with_capacity(frame.arguments().len()); - - for arg in frame.arguments() { - let Some(another) = from_js(global, *arg)? else { - return Err(global.throw_invalid_argument_type( - bname($name), - "additional arguments", - "string or buffer", - )); - }; - args.push(another); - } - send_cmd( - this, - global, - frame.this(), - $command.as_bytes(), - CommandArgs::Args(&args), - CommandMeta::default(), - concat!("Failed to send ", $command), - ) - } + ($fn_name:ident, $name:literal, $command:literal, strict, $state:ident $(,)?) => { + cmd_varargs!(@impl $fn_name, $name, $command, false, $state); }; -} - -macro_rules! cmd_key_value_varargs { - ($fn_name:ident, $name:literal, $command:literal, $state:ident) => { + (@impl $fn_name:ident, $name:literal, $command:literal, $skip_null:literal, $state:ident $(, $arg0_name:literal)?) => { #[bun_jsc::host_fn(method)] pub fn $fn_name( this: &Self, @@ -405,11 +234,20 @@ macro_rules! cmd_key_value_varargs { this, $name, )?; - let mut args: Vec = Vec::with_capacity(frame.arguments().len()); + $( + if frame.argument(0).is_undefined_or_null() { + return Err(global.throw_missing_arguments_value(&[$arg0_name])); + } + )? - for arg in frame.arguments() { - if arg.is_undefined_or_null() { - continue; + let arguments = frame.arguments(); + let mut args: Vec = Vec::with_capacity(arguments.len()); + + for arg in arguments { + if $skip_null { + if arg.is_undefined_or_null() { + continue; + } } let Some(another) = from_js(global, *arg)? else { @@ -1118,27 +956,45 @@ impl JSValkeyClient { Self::hset_impl(this, global, frame, b"HMSET") } - cmd_key_varargs!(hdel, b"hdel", "HDEL", "key", NotSubscriber); - cmd_key_varargs!( + cmd_varargs!(hdel, b"hdel", "HDEL", required "key", NotSubscriber); + cmd_varargs!( hrandfield, b"hrandfield", "HRANDFIELD", - "key", + required "key", + NotSubscriber + ); + cmd_varargs!(hscan, b"hscan", "HSCAN", required "key", NotSubscriber); + cmd_varargs!(hgetdel, b"hgetdel", "HGETDEL", strict, NotSubscriber); + cmd_varargs!(hgetex, b"hgetex", "HGETEX", strict, NotSubscriber); + cmd_varargs!(hsetex, b"hsetex", "HSETEX", strict, NotSubscriber); + cmd_varargs!(hexpire, b"hexpire", "HEXPIRE", strict, NotSubscriber); + cmd_varargs!(hexpireat, b"hexpireat", "HEXPIREAT", strict, NotSubscriber); + cmd_varargs!( + hexpiretime, + b"hexpiretime", + "HEXPIRETIME", + strict, NotSubscriber ); - cmd_key_varargs!(hscan, b"hscan", "HSCAN", "key", NotSubscriber); - cmd_strings_varargs!(hgetdel, b"hgetdel", "HGETDEL", NotSubscriber); - cmd_strings_varargs!(hgetex, b"hgetex", "HGETEX", NotSubscriber); - cmd_strings_varargs!(hsetex, b"hsetex", "HSETEX", NotSubscriber); - cmd_strings_varargs!(hexpire, b"hexpire", "HEXPIRE", NotSubscriber); - cmd_strings_varargs!(hexpireat, b"hexpireat", "HEXPIREAT", NotSubscriber); - cmd_strings_varargs!(hexpiretime, b"hexpiretime", "HEXPIRETIME", NotSubscriber); - cmd_strings_varargs!(hpersist, b"hpersist", "HPERSIST", NotSubscriber); - cmd_strings_varargs!(hpexpire, b"hpexpire", "HPEXPIRE", NotSubscriber); - cmd_strings_varargs!(hpexpireat, b"hpexpireat", "HPEXPIREAT", NotSubscriber); - cmd_strings_varargs!(hpexpiretime, b"hpexpiretime", "HPEXPIRETIME", NotSubscriber); - cmd_strings_varargs!(hpttl, b"hpttl", "HPTTL", NotSubscriber); - cmd_strings_varargs!(httl, b"httl", "HTTL", NotSubscriber); + cmd_varargs!(hpersist, b"hpersist", "HPERSIST", strict, NotSubscriber); + cmd_varargs!(hpexpire, b"hpexpire", "HPEXPIRE", strict, NotSubscriber); + cmd_varargs!( + hpexpireat, + b"hpexpireat", + "HPEXPIREAT", + strict, + NotSubscriber + ); + cmd_varargs!( + hpexpiretime, + b"hpexpiretime", + "HPEXPIRETIME", + strict, + NotSubscriber + ); + cmd_varargs!(hpttl, b"hpttl", "HPTTL", strict, NotSubscriber); + cmd_varargs!(httl, b"httl", "HTTL", strict, NotSubscriber); #[bun_jsc::host_fn(method)] pub fn hsetnx(this: &Self, global: &JSGlobalObject, frame: &CallFrame) -> JsResult { @@ -1217,12 +1073,12 @@ impl JSValkeyClient { ) } - cmd_key!(bitcount, b"bitcount", "BITCOUNT", "key", NotSubscriber); - cmd_strings_varargs!(blmove, b"blmove", "BLMOVE", NotSubscriber); - cmd_strings_varargs!(blmpop, b"blmpop", "BLMPOP", NotSubscriber); - cmd_strings_varargs!(blpop, b"blpop", "BLPOP", NotSubscriber); - cmd_strings_varargs!(brpop, b"brpop", "BRPOP", NotSubscriber); - cmd_key_value_value2!( + cmd!(bitcount, b"bitcount", "BITCOUNT", "key", NotSubscriber); + cmd_varargs!(blmove, b"blmove", "BLMOVE", strict, NotSubscriber); + cmd_varargs!(blmpop, b"blmpop", "BLMPOP", strict, NotSubscriber); + cmd_varargs!(blpop, b"blpop", "BLPOP", strict, NotSubscriber); + cmd_varargs!(brpop, b"brpop", "BRPOP", strict, NotSubscriber); + cmd!( brpoplpush, b"brpoplpush", "BRPOPLPUSH", @@ -1231,8 +1087,8 @@ impl JSValkeyClient { "timeout", NotSubscriber ); - cmd_key_value!(getbit, b"getbit", "GETBIT", "key", "offset", NotSubscriber); - cmd_key_value_value2!( + cmd!(getbit, b"getbit", "GETBIT", "key", "offset", NotSubscriber); + cmd!( setbit, b"setbit", "SETBIT", @@ -1241,7 +1097,7 @@ impl JSValkeyClient { "value", NotSubscriber ); - cmd_key_value_value2!( + cmd!( getrange, b"getrange", "GETRANGE", @@ -1250,7 +1106,7 @@ impl JSValkeyClient { "end", NotSubscriber ); - cmd_key_value_value2!( + cmd!( setrange, b"setrange", "SETRANGE", @@ -1259,8 +1115,8 @@ impl JSValkeyClient { "value", NotSubscriber ); - cmd_key!(dump, b"dump", "DUMP", "key", NotSubscriber); - cmd_key_value!( + cmd!(dump, b"dump", "DUMP", "key", NotSubscriber); + cmd!( expireat, b"expireat", "EXPIREAT", @@ -1268,28 +1124,28 @@ impl JSValkeyClient { "timestamp", NotSubscriber ); - cmd_key!( + cmd!( expiretime, b"expiretime", "EXPIRETIME", "key", NotSubscriber ); - cmd_key!(getdel, b"getdel", "GETDEL", "key", NotSubscriber); - cmd_strings_varargs!(getex, b"getex", "GETEX", NotSubscriber); - cmd_key!(hgetall, b"hgetall", "HGETALL", "key", NotSubscriber); - cmd_key!(hkeys, b"hkeys", "HKEYS", "key", NotSubscriber); - cmd_key!(hlen, b"hlen", "HLEN", "key", NotSubscriber); - cmd_key!(hvals, b"hvals", "HVALS", "key", NotSubscriber); - cmd_key!(keys, b"keys", "KEYS", "key", NotSubscriber); - cmd_key_value!(lindex, b"lindex", "LINDEX", "key", "index", NotSubscriber); - cmd_strings_varargs!(linsert, b"linsert", "LINSERT", NotSubscriber); - cmd_key!(llen, b"llen", "LLEN", "key", NotSubscriber); - cmd_strings_varargs!(lmove, b"lmove", "LMOVE", NotSubscriber); - cmd_strings_varargs!(lmpop, b"lmpop", "LMPOP", NotSubscriber); - cmd_key_varargs!(lpop, b"lpop", "LPOP", "key", NotSubscriber); - cmd_strings_varargs!(lpos, b"lpos", "LPOS", NotSubscriber); - cmd_key_value_value2!( + cmd!(getdel, b"getdel", "GETDEL", "key", NotSubscriber); + cmd_varargs!(getex, b"getex", "GETEX", strict, NotSubscriber); + cmd!(hgetall, b"hgetall", "HGETALL", "key", NotSubscriber); + cmd!(hkeys, b"hkeys", "HKEYS", "key", NotSubscriber); + cmd!(hlen, b"hlen", "HLEN", "key", NotSubscriber); + cmd!(hvals, b"hvals", "HVALS", "key", NotSubscriber); + cmd!(keys, b"keys", "KEYS", "key", NotSubscriber); + cmd!(lindex, b"lindex", "LINDEX", "key", "index", NotSubscriber); + cmd_varargs!(linsert, b"linsert", "LINSERT", strict, NotSubscriber); + cmd!(llen, b"llen", "LLEN", "key", NotSubscriber); + cmd_varargs!(lmove, b"lmove", "LMOVE", strict, NotSubscriber); + cmd_varargs!(lmpop, b"lmpop", "LMPOP", strict, NotSubscriber); + cmd_varargs!(lpop, b"lpop", "LPOP", required "key", NotSubscriber); + cmd_varargs!(lpos, b"lpos", "LPOS", strict, NotSubscriber); + cmd!( lrange, b"lrange", "LRANGE", @@ -1298,7 +1154,7 @@ impl JSValkeyClient { "stop", NotSubscriber ); - cmd_key_value_value2!( + cmd!( lrem, b"lrem", "LREM", @@ -1307,7 +1163,7 @@ impl JSValkeyClient { "element", NotSubscriber ); - cmd_key_value_value2!( + cmd!( lset, b"lset", "LSET", @@ -1316,7 +1172,7 @@ impl JSValkeyClient { "element", NotSubscriber ); - cmd_key_value_value2!( + cmd!( ltrim, b"ltrim", "LTRIM", @@ -1325,8 +1181,8 @@ impl JSValkeyClient { "stop", NotSubscriber ); - cmd_key!(persist, b"persist", "PERSIST", "key", NotSubscriber); - cmd_key_value!( + cmd!(persist, b"persist", "PERSIST", "key", NotSubscriber); + cmd!( pexpire, b"pexpire", "PEXPIRE", @@ -1334,7 +1190,7 @@ impl JSValkeyClient { "milliseconds", NotSubscriber ); - cmd_key_value!( + cmd!( pexpireat, b"pexpireat", "PEXPIREAT", @@ -1342,17 +1198,17 @@ impl JSValkeyClient { "milliseconds-timestamp", NotSubscriber ); - cmd_key!( + cmd!( pexpiretime, b"pexpiretime", "PEXPIRETIME", "key", NotSubscriber ); - cmd_key!(pttl, b"pttl", "PTTL", "key", NotSubscriber); - cmd_noargs!(randomkey, b"randomkey", "RANDOMKEY", NotSubscriber); - cmd_key_varargs!(rpop, b"rpop", "RPOP", "key", NotSubscriber); - cmd_key_value!( + cmd!(pttl, b"pttl", "PTTL", "key", NotSubscriber); + cmd!(randomkey, b"randomkey", "RANDOMKEY", NotSubscriber); + cmd_varargs!(rpop, b"rpop", "RPOP", required "key", NotSubscriber); + cmd!( rpoplpush, b"rpoplpush", "RPOPLPUSH", @@ -1360,21 +1216,51 @@ impl JSValkeyClient { "destination", NotSubscriber ); - cmd_strings_varargs!(scan, b"scan", "SCAN", NotSubscriber); - cmd_key!(scard, b"scard", "SCARD", "key", NotSubscriber); - cmd_strings_varargs!(sdiff, b"sdiff", "SDIFF", NotSubscriber); - cmd_strings_varargs!(sdiffstore, b"sdiffstore", "SDIFFSTORE", NotSubscriber); - cmd_strings_varargs!(sinter, b"sinter", "SINTER", NotSubscriber); - cmd_strings_varargs!(sintercard, b"sintercard", "SINTERCARD", NotSubscriber); - cmd_strings_varargs!(sinterstore, b"sinterstore", "SINTERSTORE", NotSubscriber); - cmd_strings_varargs!(smismember, b"smismember", "SMISMEMBER", NotSubscriber); - cmd_strings_varargs!(sscan, b"sscan", "SSCAN", NotSubscriber); - cmd_key!(strlen, b"strlen", "STRLEN", "key", NotSubscriber); - cmd_strings_varargs!(sunion, b"sunion", "SUNION", NotSubscriber); - cmd_strings_varargs!(sunionstore, b"sunionstore", "SUNIONSTORE", NotSubscriber); - cmd_key!(r#type, b"type", "TYPE", "key", NotSubscriber); - cmd_key!(zcard, b"zcard", "ZCARD", "key", NotSubscriber); - cmd_key_value_value2!( + cmd_varargs!(scan, b"scan", "SCAN", strict, NotSubscriber); + cmd!(scard, b"scard", "SCARD", "key", NotSubscriber); + cmd_varargs!(sdiff, b"sdiff", "SDIFF", strict, NotSubscriber); + cmd_varargs!( + sdiffstore, + b"sdiffstore", + "SDIFFSTORE", + strict, + NotSubscriber + ); + cmd_varargs!(sinter, b"sinter", "SINTER", strict, NotSubscriber); + cmd_varargs!( + sintercard, + b"sintercard", + "SINTERCARD", + strict, + NotSubscriber + ); + cmd_varargs!( + sinterstore, + b"sinterstore", + "SINTERSTORE", + strict, + NotSubscriber + ); + cmd_varargs!( + smismember, + b"smismember", + "SMISMEMBER", + strict, + NotSubscriber + ); + cmd_varargs!(sscan, b"sscan", "SSCAN", strict, NotSubscriber); + cmd!(strlen, b"strlen", "STRLEN", "key", NotSubscriber); + cmd_varargs!(sunion, b"sunion", "SUNION", strict, NotSubscriber); + cmd_varargs!( + sunionstore, + b"sunionstore", + "SUNIONSTORE", + strict, + NotSubscriber + ); + cmd!(r#type, b"type", "TYPE", "key", NotSubscriber); + cmd!(zcard, b"zcard", "ZCARD", "key", NotSubscriber); + cmd!( zcount, b"zcount", "ZCOUNT", @@ -1383,7 +1269,7 @@ impl JSValkeyClient { "max", NotSubscriber ); - cmd_key_value_value2!( + cmd!( zlexcount, b"zlexcount", "ZLEXCOUNT", @@ -1392,47 +1278,49 @@ impl JSValkeyClient { "max", NotSubscriber ); - cmd_key_varargs!(zpopmax, b"zpopmax", "ZPOPMAX", "key", NotSubscriber); - cmd_key_varargs!(zpopmin, b"zpopmin", "ZPOPMIN", "key", NotSubscriber); - cmd_key_varargs!( + cmd_varargs!(zpopmax, b"zpopmax", "ZPOPMAX", required "key", NotSubscriber); + cmd_varargs!(zpopmin, b"zpopmin", "ZPOPMIN", required "key", NotSubscriber); + cmd_varargs!( zrandmember, b"zrandmember", "ZRANDMEMBER", - "key", + required "key", NotSubscriber ); - cmd_strings_varargs!(zrange, b"zrange", "ZRANGE", NotSubscriber); - cmd_strings_varargs!(zrevrange, b"zrevrange", "ZREVRANGE", NotSubscriber); - cmd_strings_varargs!( + cmd_varargs!(zrange, b"zrange", "ZRANGE", strict, NotSubscriber); + cmd_varargs!(zrevrange, b"zrevrange", "ZREVRANGE", strict, NotSubscriber); + cmd_varargs!( zrangebyscore, b"zrangebyscore", "ZRANGEBYSCORE", + strict, NotSubscriber ); - cmd_strings_varargs!( + cmd_varargs!( zrevrangebyscore, b"zrevrangebyscore", "ZREVRANGEBYSCORE", + strict, NotSubscriber ); - cmd_key_varargs!( + cmd_varargs!( zrangebylex, b"zrangebylex", "ZRANGEBYLEX", - "key", + required "key", NotSubscriber ); - cmd_key_varargs!( + cmd_varargs!( zrevrangebylex, b"zrevrangebylex", "ZREVRANGEBYLEX", - "key", + required "key", NotSubscriber ); - cmd_key_value!(append, b"append", "APPEND", "key", "value", NotSubscriber); - cmd_key_value!(getset, b"getset", "GETSET", "key", "value", NotSubscriber); - cmd_key_value!(hget, b"hget", "HGET", "key", "field", NotSubscriber); - cmd_key_value!( + cmd!(append, b"append", "APPEND", "key", "value", NotSubscriber); + cmd!(getset, b"getset", "GETSET", "key", "value", NotSubscriber); + cmd!(hget, b"hget", "HGET", "key", "field", NotSubscriber); + cmd!( incrby, b"incrby", "INCRBY", @@ -1440,7 +1328,7 @@ impl JSValkeyClient { "increment", NotSubscriber ); - cmd_key_value!( + cmd!( incrbyfloat, b"incrbyfloat", "INCRBYFLOAT", @@ -1448,7 +1336,7 @@ impl JSValkeyClient { "increment", NotSubscriber ); - cmd_key_value!( + cmd!( decrby, b"decrby", "DECRBY", @@ -1456,13 +1344,13 @@ impl JSValkeyClient { "decrement", NotSubscriber ); - cmd_key_value_varargs!(lpush, b"lpush", "LPUSH", NotSubscriber); - cmd_key_value_varargs!(lpushx, b"lpushx", "LPUSHX", NotSubscriber); - cmd_key_value!(pfadd, b"pfadd", "PFADD", "key", "value", NotSubscriber); - cmd_key_value_varargs!(rpush, b"rpush", "RPUSH", NotSubscriber); - cmd_key_value_varargs!(rpushx, b"rpushx", "RPUSHX", NotSubscriber); - cmd_key_value!(setnx, b"setnx", "SETNX", "key", "value", NotSubscriber); - cmd_key_value_value2!( + cmd_varargs!(lpush, b"lpush", "LPUSH", skip_null, NotSubscriber); + cmd_varargs!(lpushx, b"lpushx", "LPUSHX", skip_null, NotSubscriber); + cmd!(pfadd, b"pfadd", "PFADD", "key", "value", NotSubscriber); + cmd_varargs!(rpush, b"rpush", "RPUSH", skip_null, NotSubscriber); + cmd_varargs!(rpushx, b"rpushx", "RPUSHX", skip_null, NotSubscriber); + cmd!(setnx, b"setnx", "SETNX", "key", "value", NotSubscriber); + cmd!( setex, b"setex", "SETEX", @@ -1471,7 +1359,7 @@ impl JSValkeyClient { "value", NotSubscriber ); - cmd_key_value_value2!( + cmd!( psetex, b"psetex", "PSETEX", @@ -1480,8 +1368,8 @@ impl JSValkeyClient { "value", NotSubscriber ); - cmd_key_value!(zscore, b"zscore", "ZSCORE", "key", "value", NotSubscriber); - cmd_key_value_value2!( + cmd!(zscore, b"zscore", "ZSCORE", "key", "value", NotSubscriber); + cmd!( zincrby, b"zincrby", "ZINCRBY", @@ -1490,27 +1378,51 @@ impl JSValkeyClient { "member", NotSubscriber ); - cmd_key_value_varargs!(zmscore, b"zmscore", "ZMSCORE", NotSubscriber); - cmd_strings_varargs!(zadd, b"zadd", "ZADD", NotSubscriber); - cmd_strings_varargs!(zscan, b"zscan", "ZSCAN", NotSubscriber); - cmd_strings_varargs!(zdiff, b"zdiff", "ZDIFF", NotSubscriber); - cmd_strings_varargs!(zdiffstore, b"zdiffstore", "ZDIFFSTORE", NotSubscriber); - cmd_strings_varargs!(zinter, b"zinter", "ZINTER", NotSubscriber); - cmd_strings_varargs!(zintercard, b"zintercard", "ZINTERCARD", NotSubscriber); - cmd_strings_varargs!(zinterstore, b"zinterstore", "ZINTERSTORE", NotSubscriber); - cmd_strings_varargs!(zunion, b"zunion", "ZUNION", NotSubscriber); - cmd_strings_varargs!(zunionstore, b"zunionstore", "ZUNIONSTORE", NotSubscriber); - cmd_strings_varargs!(zmpop, b"zmpop", "ZMPOP", NotSubscriber); - cmd_strings_varargs!(bzmpop, b"bzmpop", "BZMPOP", NotSubscriber); - cmd_strings_varargs!(bzpopmin, b"bzpopmin", "BZPOPMIN", NotSubscriber); - cmd_strings_varargs!(bzpopmax, b"bzpopmax", "BZPOPMAX", NotSubscriber); - cmd_key_varargs!(del, b"del", "DEL", "key", NotSubscriber); - cmd_key_varargs!(mget, b"mget", "MGET", "key", NotSubscriber); - cmd_strings_varargs!(mset, b"mset", "MSET", NotSubscriber); - cmd_strings_varargs!(msetnx, b"msetnx", "MSETNX", NotSubscriber); - cmd_strings_varargs!(script, b"script", "SCRIPT", NotSubscriber); - cmd_strings_varargs!(select, b"select", "SELECT", NotSubscriber); - cmd_key_value!( + cmd_varargs!(zmscore, b"zmscore", "ZMSCORE", skip_null, NotSubscriber); + cmd_varargs!(zadd, b"zadd", "ZADD", strict, NotSubscriber); + cmd_varargs!(zscan, b"zscan", "ZSCAN", strict, NotSubscriber); + cmd_varargs!(zdiff, b"zdiff", "ZDIFF", strict, NotSubscriber); + cmd_varargs!( + zdiffstore, + b"zdiffstore", + "ZDIFFSTORE", + strict, + NotSubscriber + ); + cmd_varargs!(zinter, b"zinter", "ZINTER", strict, NotSubscriber); + cmd_varargs!( + zintercard, + b"zintercard", + "ZINTERCARD", + strict, + NotSubscriber + ); + cmd_varargs!( + zinterstore, + b"zinterstore", + "ZINTERSTORE", + strict, + NotSubscriber + ); + cmd_varargs!(zunion, b"zunion", "ZUNION", strict, NotSubscriber); + cmd_varargs!( + zunionstore, + b"zunionstore", + "ZUNIONSTORE", + strict, + NotSubscriber + ); + cmd_varargs!(zmpop, b"zmpop", "ZMPOP", strict, NotSubscriber); + cmd_varargs!(bzmpop, b"bzmpop", "BZMPOP", strict, NotSubscriber); + cmd_varargs!(bzpopmin, b"bzpopmin", "BZPOPMIN", strict, NotSubscriber); + cmd_varargs!(bzpopmax, b"bzpopmax", "BZPOPMAX", strict, NotSubscriber); + cmd_varargs!(del, b"del", "DEL", required "key", NotSubscriber); + cmd_varargs!(mget, b"mget", "MGET", required "key", NotSubscriber); + cmd_varargs!(mset, b"mset", "MSET", strict, NotSubscriber); + cmd_varargs!(msetnx, b"msetnx", "MSETNX", strict, NotSubscriber); + cmd_varargs!(script, b"script", "SCRIPT", strict, NotSubscriber); + cmd_varargs!(select, b"select", "SELECT", strict, NotSubscriber); + cmd!( spublish, b"spublish", "SPUBLISH", @@ -1547,7 +1459,7 @@ impl JSValkeyClient { ) } - cmd_key_value_value2!( + cmd!( substr, b"substr", "SUBSTR", @@ -1556,7 +1468,7 @@ impl JSValkeyClient { "end", NotSubscriber ); - cmd_key_value!( + cmd!( hstrlen, b"hstrlen", "HSTRLEN", @@ -1564,10 +1476,16 @@ impl JSValkeyClient { "field", NotSubscriber ); - cmd_key_varargs!(zrank, b"zrank", "ZRANK", "key", NotSubscriber); - cmd_strings_varargs!(zrangestore, b"zrangestore", "ZRANGESTORE", NotSubscriber); - cmd_key_varargs!(zrem, b"zrem", "ZREM", "key", NotSubscriber); - cmd_key_value_value2!( + cmd_varargs!(zrank, b"zrank", "ZRANK", required "key", NotSubscriber); + cmd_varargs!( + zrangestore, + b"zrangestore", + "ZRANGESTORE", + strict, + NotSubscriber + ); + cmd_varargs!(zrem, b"zrem", "ZREM", required "key", NotSubscriber); + cmd!( zremrangebylex, b"zremrangebylex", "ZREMRANGEBYLEX", @@ -1576,7 +1494,7 @@ impl JSValkeyClient { "max", NotSubscriber ); - cmd_key_value_value2!( + cmd!( zremrangebyrank, b"zremrangebyrank", "ZREMRANGEBYRANK", @@ -1585,7 +1503,7 @@ impl JSValkeyClient { "stop", NotSubscriber ); - cmd_key_value_value2!( + cmd!( zremrangebyscore, b"zremrangebyscore", "ZREMRANGEBYSCORE", @@ -1594,15 +1512,27 @@ impl JSValkeyClient { "max", NotSubscriber ); - cmd_key_varargs!(zrevrank, b"zrevrank", "ZREVRANK", "key", NotSubscriber); - cmd_strings_varargs!(psubscribe, b"psubscribe", "PSUBSCRIBE", DontCare); - cmd_strings_varargs!(punsubscribe, b"punsubscribe", "PUNSUBSCRIBE", DontCare); - cmd_strings_varargs!(pubsub, b"pubsub", "PUBSUB", DontCare); - cmd_strings_varargs!(copy, b"copy", "COPY", NotSubscriber); - cmd_key_varargs!(unlink, b"unlink", "UNLINK", "key", NotSubscriber); - cmd_key_varargs!(touch, b"touch", "TOUCH", "key", NotSubscriber); - cmd_key_value!(rename, b"rename", "RENAME", "key", "newkey", NotSubscriber); - cmd_key_value!( + cmd_varargs!( + zrevrank, + b"zrevrank", + "ZREVRANK", + required "key", + NotSubscriber + ); + cmd_varargs!(psubscribe, b"psubscribe", "PSUBSCRIBE", strict, DontCare); + cmd_varargs!( + punsubscribe, + b"punsubscribe", + "PUNSUBSCRIBE", + strict, + DontCare + ); + cmd_varargs!(pubsub, b"pubsub", "PUBSUB", strict, DontCare); + cmd_varargs!(copy, b"copy", "COPY", strict, NotSubscriber); + cmd_varargs!(unlink, b"unlink", "UNLINK", required "key", NotSubscriber); + cmd_varargs!(touch, b"touch", "TOUCH", required "key", NotSubscriber); + cmd!(rename, b"rename", "RENAME", "key", "newkey", NotSubscriber); + cmd!( renamenx, b"renamenx", "RENAMENX", diff --git a/src/runtime/webcore/ArrayBufferSink.rs b/src/runtime/webcore/ArrayBufferSink.rs index 4f0bd90b1a49..91a2cbabc70d 100644 --- a/src/runtime/webcore/ArrayBufferSink.rs +++ b/src/runtime/webcore/ArrayBufferSink.rs @@ -260,9 +260,8 @@ impl crate::webcore::sink::JsSinkType for ArrayBufferSink { const HAS_FLUSH_FROM_JS: bool = true; const START_TAG: Option = Some(streams::StartTag::ArrayBufferSink); - fn memory_cost(&self) -> usize { - Self::memory_cost(self) - } + crate::impl_js_sink_forwarders!(); + fn finalize(&mut self) { // The `JSSink::finalize` C export owns destroying the heap // allocation; the trait impl here is the *inner* finalize. @@ -271,18 +270,6 @@ impl crate::webcore::sink::JsSinkType for ArrayBufferSink { fn construct(this: &mut core::mem::MaybeUninit) { Self::construct(this); } - fn write_bytes(&mut self, data: &streams::Result) -> streams::result::Writable { - Self::write(self, data) - } - fn write_utf16(&mut self, data: &streams::Result) -> streams::result::Writable { - Self::write_utf16(self, data) - } - fn write_latin1(&mut self, data: &streams::Result) -> streams::result::Writable { - Self::write_latin1(self, data) - } - fn end(&mut self, err: Option) -> bun_sys::Result<()> { - Self::end(self, err) - } fn end_from_js(&mut self, global: &JSGlobalObject) -> bun_sys::Result { match Self::end_from_js(self, global) { bun_sys::Result::Ok(ab) => bun_sys::Result::Ok(match ab.to_js_unchecked(global) { @@ -292,15 +279,6 @@ impl crate::webcore::sink::JsSinkType for ArrayBufferSink { bun_sys::Result::Err(e) => bun_sys::Result::Err(e), } } - fn flush(&mut self) -> bun_sys::Result<()> { - Self::flush(self) - } - fn flush_from_js(&mut self, global: &JSGlobalObject, wait: bool) -> bun_sys::Result { - Self::flush_from_js(self, global, wait) - } - fn start(&mut self, config: streams::Start) -> bun_sys::Result<()> { - Self::start(self, &config) - } fn signal(&mut self) -> Option<&mut Signal> { Some(&mut self.signal) } diff --git a/src/runtime/webcore/Blob.rs b/src/runtime/webcore/Blob.rs index 49348281ff42..84aa5dc0253e 100644 --- a/src/runtime/webcore/Blob.rs +++ b/src/runtime/webcore/Blob.rs @@ -1337,31 +1337,7 @@ impl BlobExt for Blob { } if let Some(content_type) = options_object.get_truthy(global_this, "type")? { // override the content type - if !content_type.is_string() { - return Err(global_this.throw_invalid_argument_type( - "write", - "options.type", - "string", - )); - } - let content_type_str = content_type.to_slice(global_this)?; - let slice = content_type_str.slice(); - if is_valid_blob_type(slice) { - self.free_content_type(); - self.content_type_was_set.set(true); - - // SAFETY: bun_vm() never returns null for a Bun-owned global. - if let Some(mime) = global_this.bun_vm().as_mut().mime_type(slice) { - self.content_type - .set(std::ptr::from_ref::<[u8]>(mime.value.as_ref())); - } else { - let mut buf = vec![0u8; slice.len()]; - strings::copy_lowercase(slice, &mut buf); - self.content_type - .set(bun_core::heap::into_raw(buf.into_boxed_slice())); - self.content_type_allocated.set(true); - } - } + set_content_type_from_js(global_this, self, content_type)?; } } else if !options_object.is_empty_or_undefined_or_null() { return Err(global_this.throw_invalid_argument_type("write", "options", "object")); @@ -1767,30 +1743,7 @@ impl BlobExt for Blob { let options = arg0; if let Some(content_type) = options.get_truthy(global_this, "type")? { // override the content type - if !content_type.is_string() { - return Err(global_this.throw_invalid_argument_type( - "write", - "options.type", - "string", - )); - } - let content_type_str = content_type.to_slice(global_this)?; - let slice = content_type_str.slice(); - if is_valid_blob_type(slice) { - self.free_content_type(); - self.content_type_was_set.set(true); - // SAFETY: see other `mime_type` call sites. - if let Some(mime) = global_this.bun_vm().as_mut().mime_type(slice) { - self.content_type - .set(std::ptr::from_ref::<[u8]>(mime.value.as_ref())); - } else { - let mut buf = vec![0u8; slice.len()]; - strings::copy_lowercase(slice, &mut buf); - self.content_type - .set(bun_core::heap::into_raw(buf.into_boxed_slice())); - self.content_type_allocated.set(true); - } - } + set_content_type_from_js(global_this, self, content_type)?; } let content_disposition_str: Option = @@ -3369,20 +3322,7 @@ impl BlobExt for Blob { } } - 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() => { return Blob::try_create( top_value.as_array_buffer(global).unwrap().byte_slice(), global, @@ -3517,21 +3457,8 @@ impl BlobExt for Blob { continue; } match item.js_type_loose() { - jsc::JSType::String - | 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 => {} + jsc::JSType::String => {} + t if t.is_array_buffer_like() => {} jsc::JSType::DOMWrapper if item.as_class_ref::().is_some() => {} _ => { @@ -3560,20 +3487,7 @@ impl BlobExt for Blob { joiner.push_cloned(sliced.slice()); continue; } - 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() => { could_have_non_ascii = true; let buf = item.as_array_buffer(global).unwrap(); if parts_can_run_js { @@ -3650,20 +3564,7 @@ impl BlobExt for Blob { } } - 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() => { let buf = current.as_array_buffer(global).unwrap(); // SAFETY: this arm is only reached when the typed array is the // top-level value (the walk stack is empty), so no user JS runs @@ -5343,6 +5244,36 @@ fn validate_writable_blob(global_this: &JSGlobalObject, blob: &Blob) -> JsResult Ok(()) } +/// Overrides `blob`'s content type from a write-path `options.type` value. +/// Throws if the value is not a string; silently ignores invalid blob types. +fn set_content_type_from_js( + global_this: &JSGlobalObject, + blob: &Blob, + content_type: JSValue, +) -> JsResult<()> { + if !content_type.is_string() { + return Err(global_this.throw_invalid_argument_type("write", "options.type", "string")); + } + let content_type_str = content_type.to_slice(global_this)?; + let slice = content_type_str.slice(); + if is_valid_blob_type(slice) { + blob.free_content_type(); + blob.content_type_was_set.set(true); + // SAFETY: bun_vm() never returns null for a Bun-owned global. + if let Some(mime) = global_this.bun_vm().as_mut().mime_type(slice) { + blob.content_type + .set(std::ptr::from_ref::<[u8]>(mime.value.as_ref())); + } else { + let mut buf = vec![0u8; slice.len()]; + strings::copy_lowercase(slice, &mut buf); + blob.content_type + .set(bun_core::heap::into_raw(buf.into_boxed_slice())); + blob.content_type_allocated.set(true); + } + } + Ok(()) +} + /// `Bun.write(destination, input, options?)` pub fn write_file(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { let arguments = callframe.arguments(); @@ -7335,6 +7266,93 @@ pub trait FileCloser: Sized { } } +/// Implements [`FileCloser`] for a struct with the standard field set +/// (`opened_fd`, `close_after_io`, `state`, `io_request`, `io_poll`, `task`), +/// an inherent `update()`, and a [`bun_io::Tag`] variant named after the type. +/// Requires `bun_threading::intrusive_work_task!` and +/// `bun_io::intrusive_io_request!` on the type for the intrusive-pointer +/// recovery in the two trampolines. +macro_rules! impl_file_closer { + ($T:ident) => { + impl crate::webcore::blob::FileCloser for $T { + const IO_TAG: ::bun_io::Tag = ::bun_io::Tag::$T; + fn opened_fd(&self) -> ::bun_sys::Fd { + self.opened_fd + } + fn set_opened_fd(&mut self, fd: ::bun_sys::Fd) { + self.opened_fd = fd; + } + fn close_after_io(&self) -> bool { + self.close_after_io + } + fn set_close_after_io(&mut self, v: bool) { + self.close_after_io = v; + } + fn state(&self) -> &::core::sync::atomic::AtomicU8 { + &self.state + } + fn io_request(&mut self) -> Option<&mut ::bun_io::Request> { + Some(&mut self.io_request) + } + fn io_poll(&mut self) -> &mut ::bun_io::Poll { + &mut self.io_poll + } + fn task(&mut self) -> &mut ::bun_jsc::WorkPoolTask { + &mut self.task + } + fn update(&mut self) { + $T::update(self) + } + #[cfg(windows)] + fn loop_(&self) -> *mut ::bun_libuv_sys::uv_loop_t { + unreachable!() + } + + fn schedule_close(request: &mut ::bun_io::Request) -> ::bun_io::Action<'_> { + // SAFETY: request is &mut self.io_request (intrusive); recover parent. + let this = unsafe { + &mut *<$T as ::bun_io::IntrusiveIoRequest>::from_io_request( + ::core::ptr::from_mut(request), + ) + }; + fn on_done(ctx: *mut ()) { + // SAFETY: ctx is `self as *mut Self` set below. + let this = unsafe { ::bun_ptr::callback_ctx::<$T>(ctx.cast()) }; + <$T as crate::webcore::blob::FileCloser>::on_io_request_closed(this); + } + // reshaped for borrowck — compute the parent raw pointer + // before mutably borrowing `io_poll` so the two borrows do not overlap. + let ctx = ::core::ptr::from_mut::<$T>(this).cast::<()>(); + let fd = this.opened_fd; + ::bun_io::Action::Close(::bun_io::CloseAction { + fd, + poll: &mut this.io_poll, + ctx, + tag: ::IO_TAG, + on_done, + }) + } + + // `FileCloser` fixes `on_close_io_request` to take `*mut WorkPoolTask`; + // the trait method cannot be marked `unsafe fn`, so the lint is + // unsatisfiable here. The pointer is the intrusive `&mut self.task` set + // in `on_io_request_closed` and is guaranteed live. + #[allow(clippy::not_unsafe_ptr_arg_deref)] + fn on_close_io_request(task: *mut ::bun_jsc::WorkPoolTask) { + // SAFETY: only reached via `WorkPoolTask::callback` with `task` = + // `&mut self.task` (intrusive) registered in `on_io_request_closed`; + // recover parent. + let this = unsafe { + &mut *<$T as ::bun_threading::IntrusiveWorkTask>::from_task_ptr(task) + }; + this.close_after_io = false; + $T::update(this); + } + } + }; +} +pub(crate) use impl_file_closer; + // ────────────────────────────────────────────────────────────────────────── // isAllASCII / takeOwnership / heap-alloc helpers / external_shared_descriptor // ────────────────────────────────────────────────────────────────────────── diff --git a/src/runtime/webcore/FileSink.rs b/src/runtime/webcore/FileSink.rs index 970c5e5f5000..2c5f25270b58 100644 --- a/src/runtime/webcore/FileSink.rs +++ b/src/runtime/webcore/FileSink.rs @@ -1422,9 +1422,8 @@ impl crate::webcore::sink::JsSinkType for FileSink { const HAS_GET_FD: bool = true; const START_TAG: Option = Some(streams::StartTag::FileSink); - fn memory_cost(&self) -> usize { - Self::memory_cost(self) - } + crate::impl_js_sink_forwarders!(); + fn finalize(&mut self) { Self::finalize(self) } @@ -1433,30 +1432,9 @@ impl crate::webcore::sink::JsSinkType for FileSink { // the C++ `JSFileSink` wrapper `js_construct` is about to create. this.write(Self::construct()); } - fn write_bytes(&mut self, data: &streams::Result) -> streams::result::Writable { - Self::write(self, data) - } - fn write_utf16(&mut self, data: &streams::Result) -> streams::result::Writable { - Self::write_utf16(self, data) - } - fn write_latin1(&mut self, data: &streams::Result) -> streams::result::Writable { - Self::write_latin1(self, data) - } - fn end(&mut self, err: Option) -> sys::Result<()> { - Self::end(self, err) - } fn end_from_js(&mut self, global: &JSGlobalObject) -> sys::Result { Self::end_from_js(self, global) } - fn flush(&mut self) -> sys::Result<()> { - Self::flush(self) - } - fn flush_from_js(&mut self, global: &JSGlobalObject, wait: bool) -> sys::Result { - Self::flush_from_js(self, global, wait) - } - fn start(&mut self, config: streams::Start) -> sys::Result<()> { - Self::start(self, &config) - } fn signal(&mut self) -> Option<&mut streams::Signal> { // SAFETY: JsCell — trait receiver is `&mut self`; sole borrow of `signal`. Some(unsafe { self.signal.get_mut() }) diff --git a/src/runtime/webcore/S3Client.rs b/src/runtime/webcore/S3Client.rs index 0e6cba37d083..7a21afe6f3ee 100644 --- a/src/runtime/webcore/S3Client.rs +++ b/src/runtime/webcore/S3Client.rs @@ -81,6 +81,16 @@ impl S3CredentialsExt for S3Credentials { } } +/// How [`S3Client::blob_and_options`] reports a missing/unparseable leading +/// path argument. Mirrors the per-method divergence in the Zig reference: +/// presign/exists/size/stat throw "invalid arguments" when an argument was +/// present but is not a path, while unlink always throws `MISSING_ARGS`. +#[derive(Clone, Copy)] +enum MissingPathError { + MissingOrInvalid, + AlwaysMissingArgs, +} + #[inline] fn opt_js(v: JSValue) -> Option { if v.is_empty_or_undefined_or_null() { @@ -338,6 +348,60 @@ impl S3Client { Ok(()) } + /// Constructs the S3 blob for `path` using this client's credentials and + /// per-client defaults, merged with the per-call `options` object. + /// `defer blob.detach()` from the Zig reference is handled by Drop of the + /// returned blob's `Option` field. + fn construct_blob( + &self, + global: &JSGlobalObject, + path: PathLike, + options: Option, + ) -> JsResult { + S3File::construct_s3_file_with_s3_credentials_and_options( + global, + path, + options, + &self.credentials, + self.options, + self.acl, + self.storage_class, + self.request_payer, + ) + } + + /// Shared prologue for the path-taking instance methods: parses the + /// leading path argument, eats the trailing options argument, and + /// constructs the S3 blob via [`Self::construct_blob`]. `verb` completes + /// the "Expected a path to {verb}" error message. + fn blob_and_options( + &self, + global: &JSGlobalObject, + callframe: &CallFrame, + verb: &str, + missing_path: MissingPathError, + ) -> JsResult<(crate::webcore::blob::Blob, Option)> { + let arguments = callframe.arguments_old::<2>(); + // SAFETY: `bun_vm()` returns the live VM pointer for `global`. + let vm = global.bun_vm(); + let mut args = bun_jsc::call_frame::ArgumentsSlice::init(vm, arguments.slice()); + let Some(path) = PathLike::from_js(global, &mut args)? else { + return Err(match missing_path { + MissingPathError::MissingOrInvalid if args.len() != 0 => { + global.throw_invalid_arguments(format_args!("Expected a path to {verb}")) + } + _ => global + .err( + ErrorCode::MISSING_ARGS, + format_args!("Expected a path to {verb}"), + ) + .throw(), + }); + }; + let options = args.next_eat(); + Ok((self.construct_blob(global, path, options)?, options)) + } + #[bun_jsc::host_fn(method)] pub(crate) fn file( ptr: &Self, @@ -362,18 +426,7 @@ impl S3Client { let options = args.next_eat(); // `Blob::new` heap-promotes and marks `ref_count = 1` so // the JSS3File wrapper's `finalize` knows to free the blob. - let blob = crate::webcore::blob::Blob::new( - S3File::construct_s3_file_with_s3_credentials_and_options( - global, - path, - options, - &ptr.credentials, - ptr.options, - ptr.acl, - ptr.storage_class, - ptr.request_payer, - )?, - ); + let blob = crate::webcore::blob::Blob::new(ptr.construct_blob(global, path, options)?); // `to_js` runs `calculateEstimatedByteSize()` // before wrapping the heap Blob in a JSS3File so JSC sees the correct // GC pressure. Route through `BlobExt::to_js` (the `&mut self` method @@ -389,39 +442,11 @@ impl S3Client { global: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let arguments = callframe.arguments_old::<2>(); - // SAFETY: `bun_vm()` returns the live VM pointer for `global`. - let vm = global.bun_vm(); - let mut args = bun_jsc::call_frame::ArgumentsSlice::init(vm, arguments.slice()); - let path: PathLike = match PathLike::from_js(global, &mut args)? { - Some(p) => p, - None => { - if args.len() == 0 { - return Err(global - .err( - ErrorCode::MISSING_ARGS, - format_args!("Expected a path to presign"), - ) - .throw()); - } - return Err( - global.throw_invalid_arguments(format_args!("Expected a path to presign")) - ); - } - }; - - let options = args.next_eat(); - // `defer blob.detach()` — `Blob`'s `store: Option` field - // drops at scope exit, which calls `Store::deref()` (same as detach). - let mut blob = S3File::construct_s3_file_with_s3_credentials_and_options( + let (mut blob, options) = ptr.blob_and_options( global, - path, - options, - &ptr.credentials, - ptr.options, - ptr.acl, - ptr.storage_class, - ptr.request_payer, + callframe, + "presign", + MissingPathError::MissingOrInvalid, )?; S3File::get_presign_url_from(&mut blob, global, options) } @@ -432,37 +457,11 @@ impl S3Client { global: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let arguments = callframe.arguments_old::<2>(); - // SAFETY: `bun_vm()` returns the live VM pointer for `global`. - let vm = global.bun_vm(); - let mut args = bun_jsc::call_frame::ArgumentsSlice::init(vm, arguments.slice()); - let path: PathLike = match PathLike::from_js(global, &mut args)? { - Some(p) => p, - None => { - if args.len() == 0 { - return Err(global - .err( - ErrorCode::MISSING_ARGS, - format_args!("Expected a path to check if it exists"), - ) - .throw()); - } - return Err(global.throw_invalid_arguments(format_args!( - "Expected a path to check if it exists" - ))); - } - }; - let options = args.next_eat(); - // `defer blob.detach()` — handled by Drop of `Option` field. - let blob = S3File::construct_s3_file_with_s3_credentials_and_options( + let (blob, _) = ptr.blob_and_options( global, - path, - options, - &ptr.credentials, - ptr.options, - ptr.acl, - ptr.storage_class, - ptr.request_payer, + callframe, + "check if it exists", + MissingPathError::MissingOrInvalid, )?; S3File::S3BlobStatTask::exists(global, &blob) } @@ -473,37 +472,11 @@ impl S3Client { global: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let arguments = callframe.arguments_old::<2>(); - // SAFETY: `bun_vm()` returns the live VM pointer for `global`. - let vm = global.bun_vm(); - let mut args = bun_jsc::call_frame::ArgumentsSlice::init(vm, arguments.slice()); - let path: PathLike = match PathLike::from_js(global, &mut args)? { - Some(p) => p, - None => { - if args.len() == 0 { - return Err(global - .err( - ErrorCode::MISSING_ARGS, - format_args!("Expected a path to check the size of"), - ) - .throw()); - } - return Err(global.throw_invalid_arguments(format_args!( - "Expected a path to check the size of" - ))); - } - }; - let options = args.next_eat(); - // `defer blob.detach()` — handled by Drop of `Option` field. - let mut blob = S3File::construct_s3_file_with_s3_credentials_and_options( + let (mut blob, _) = ptr.blob_and_options( global, - path, - options, - &ptr.credentials, - ptr.options, - ptr.acl, - ptr.storage_class, - ptr.request_payer, + callframe, + "check the size of", + MissingPathError::MissingOrInvalid, )?; S3File::S3BlobStatTask::size(global, &mut blob) } @@ -514,37 +487,11 @@ impl S3Client { global: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let arguments = callframe.arguments_old::<2>(); - // SAFETY: `bun_vm()` returns the live VM pointer for `global`. - let vm = global.bun_vm(); - let mut args = bun_jsc::call_frame::ArgumentsSlice::init(vm, arguments.slice()); - let path: PathLike = match PathLike::from_js(global, &mut args)? { - Some(p) => p, - None => { - if args.len() == 0 { - return Err(global - .err( - ErrorCode::MISSING_ARGS, - format_args!("Expected a path to check the stat of"), - ) - .throw()); - } - return Err(global.throw_invalid_arguments(format_args!( - "Expected a path to check the stat of" - ))); - } - }; - let options = args.next_eat(); - // `defer blob.detach()` — handled by Drop of `Option` field. - let blob = S3File::construct_s3_file_with_s3_credentials_and_options( + let (blob, _) = ptr.blob_and_options( global, - path, - options, - &ptr.credentials, - ptr.options, - ptr.acl, - ptr.storage_class, - ptr.request_payer, + callframe, + "check the stat of", + MissingPathError::MissingOrInvalid, )?; S3File::S3BlobStatTask::stat(global, &blob) } @@ -580,16 +527,7 @@ impl S3Client { }; let options = args.next_eat(); - let blob = S3File::construct_s3_file_with_s3_credentials_and_options( - global, - path, - options, - &ptr.credentials, - ptr.options, - ptr.acl, - ptr.storage_class, - ptr.request_payer, - )?; + let blob = ptr.construct_blob(global, path, options)?; // Move into `PathOrBlob` directly; cleanup of the moved-out value is // handled by `Drop`. let mut blob_internal = crate::webcore::node_types::PathOrBlob::Blob(Box::new(blob)); @@ -641,32 +579,11 @@ impl S3Client { global: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let arguments = callframe.arguments_old::<2>(); - // SAFETY: `bun_vm()` returns the live VM pointer for `global`. - let vm = global.bun_vm(); - let mut args = bun_jsc::call_frame::ArgumentsSlice::init(vm, arguments.slice()); - let path: PathLike = match PathLike::from_js(global, &mut args)? { - Some(p) => p, - None => { - return Err(global - .err( - ErrorCode::MISSING_ARGS, - format_args!("Expected a path to unlink"), - ) - .throw()); - } - }; - let options = args.next_eat(); - // `defer blob.detach()` — handled by Drop of `Option` field. - let blob = S3File::construct_s3_file_with_s3_credentials_and_options( + let (blob, options) = ptr.blob_and_options( global, - path, - options, - &ptr.credentials, - ptr.options, - ptr.acl, - ptr.storage_class, - ptr.request_payer, + callframe, + "unlink", + MissingPathError::AlwaysMissingArgs, )?; let store = blob.store.get().as_ref().unwrap(); store.data.as_s3().unlink(store, global, options) diff --git a/src/runtime/webcore/S3File.rs b/src/runtime/webcore/S3File.rs index 8122390ea106..582eab312b9b 100644 --- a/src/runtime/webcore/S3File.rs +++ b/src/runtime/webcore/S3File.rs @@ -106,14 +106,15 @@ where Ok(()) } -#[bun_jsc::host_fn] -pub(crate) fn presign(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let arguments = callframe.arguments_old::<3>(); - // SAFETY: bun_vm() returns the live VM raw ptr. - let mut args = bun_jsc::call_frame::ArgumentsSlice::init(global.bun_vm(), arguments.slice()); - +/// Shared prologue for the S3 static host fns: parse the first argument as a +/// path or Blob and require Blob arguments to be S3-backed. +fn parse_s3_path_or_blob( + global: &JSGlobalObject, + args: &mut bun_jsc::call_frame::ArgumentsSlice, + error_message: &str, +) -> JsResult { // accept a path or a blob - let path_or_blob = PathOrBlob::from_js_no_copy(global, &mut args)?; + let path_or_blob = PathOrBlob::from_js_no_copy(global, args)?; // PathOrBlob impls Drop — path variant cleaned up automatically on `?` if let PathOrBlob::Blob(blob) = &path_or_blob { @@ -123,65 +124,57 @@ pub(crate) fn presign(global: &JSGlobalObject, callframe: &CallFrame) -> JsResul blob::store::Data::S3(_) ) { - return Err( - global.throw_invalid_arguments(format_args!("Expected a S3 or path to presign")) - ); + return Err(global.throw_invalid_arguments(format_args!("{error_message}"))); } } + Ok(path_or_blob) +} +/// Resolve a parsed argument to an S3 blob: reject file-descriptor paths and +/// construct the internal S3 store for path arguments. Returns the blob along +/// with the eaten options argument. +fn resolve_s3_blob( + global: &JSGlobalObject, + args: &mut bun_jsc::call_frame::ArgumentsSlice, + path_or_blob: PathOrBlob, + error_message: &str, +) -> JsResult<(Box, Option)> { + let options = args.next_eat(); match path_or_blob { PathOrBlob::Path(path) => { if matches!(path, crate::node::PathOrFileDescriptor::Fd(_)) { - return Err(global - .throw_invalid_arguments(format_args!("Expected a S3 or path to presign"))); + return Err(global.throw_invalid_arguments(format_args!("{error_message}"))); } - let options = args.next_eat(); - let mut blob = construct_s3_file_internal_store(global, path.path().clone(), options)?; - get_presign_url_from(&mut blob, global, options) + let blob = construct_s3_file_internal_store(global, path.path().clone(), options)?; + Ok((Box::new(blob), options)) } - PathOrBlob::Blob(mut blob) => get_presign_url_from(&mut blob, global, args.next_eat()), + PathOrBlob::Blob(blob) => Ok((blob, options)), } } #[bun_jsc::host_fn] -pub(crate) fn unlink(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { +pub(crate) fn presign(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { let arguments = callframe.arguments_old::<3>(); // SAFETY: bun_vm() returns the live VM raw ptr. let mut args = bun_jsc::call_frame::ArgumentsSlice::init(global.bun_vm(), arguments.slice()); - // accept a path or a blob - let path_or_blob = PathOrBlob::from_js_no_copy(global, &mut args)?; + let error_message = "Expected a S3 or path to presign"; + let path_or_blob = parse_s3_path_or_blob(global, &mut args, error_message)?; + let (mut blob, options) = resolve_s3_blob(global, &mut args, path_or_blob, error_message)?; + get_presign_url_from(&mut blob, global, options) +} - if let PathOrBlob::Blob(blob) = &path_or_blob { - if blob.store.get().is_none() - || !matches!( - blob.store.get().as_ref().unwrap().data, - blob::store::Data::S3(_) - ) - { - return Err( - global.throw_invalid_arguments(format_args!("Expected a S3 or path to delete")) - ); - } - } +#[bun_jsc::host_fn] +pub(crate) fn unlink(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { + let arguments = callframe.arguments_old::<3>(); + // SAFETY: bun_vm() returns the live VM raw ptr. + let mut args = bun_jsc::call_frame::ArgumentsSlice::init(global.bun_vm(), arguments.slice()); - match path_or_blob { - PathOrBlob::Path(path) => { - if matches!(path, crate::node::PathOrFileDescriptor::Fd(_)) { - return Err( - global.throw_invalid_arguments(format_args!("Expected a S3 or path to delete")) - ); - } - let options = args.next_eat(); - let blob = construct_s3_file_internal_store(global, path.path().clone(), options)?; - let store = blob.store.get().as_ref().unwrap(); - store.data.as_s3().unlink(store, global, options) - } - PathOrBlob::Blob(blob) => { - let store = blob.store.get().as_ref().unwrap(); - store.data.as_s3().unlink(store, global, args.next_eat()) - } - } + let error_message = "Expected a S3 or path to delete"; + let path_or_blob = parse_s3_path_or_blob(global, &mut args, error_message)?; + let (blob, options) = resolve_s3_blob(global, &mut args, path_or_blob, error_message)?; + let store = blob.store.get().as_ref().unwrap(); + store.data.as_s3().unlink(store, global, options) } #[bun_jsc::host_fn] @@ -190,21 +183,8 @@ pub fn write(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult JsResult { - let options = args.next_eat(); - if matches!(path, crate::node::PathOrFileDescriptor::Fd(_)) { - return Err( - global.throw_invalid_arguments(format_args!("Expected a S3 or path to upload")) - ); - } - let blob = construct_s3_file_internal_store(global, path.path().clone(), options)?; - - let mut blob_internal = PathOrBlob::Blob(Box::new(blob)); - blob::write_file_internal( - global, - &mut blob_internal, - data, - blob::WriteFileOptions { - mkdirp_if_not_exists: Some(false), - extra_options: options, - ..Default::default() - }, - ) - } - PathOrBlob::Blob(blob) => { - // Reshaped for borrowck — match consumes path_or_blob; rebuild to pass &mut PathOrBlob - let mut pob = PathOrBlob::Blob(blob); - blob::write_file_internal( - global, - &mut pob, - data, - blob::WriteFileOptions { - mkdirp_if_not_exists: Some(false), - extra_options: args.next_eat(), - ..Default::default() - }, - ) - } - } + let (blob, options) = resolve_s3_blob(global, &mut args, path_or_blob, error_message)?; + // `write_file_internal` takes `&mut PathOrBlob`; wrap the resolved blob. + let mut blob_internal = PathOrBlob::Blob(blob); + blob::write_file_internal( + global, + &mut blob_internal, + data, + blob::WriteFileOptions { + mkdirp_if_not_exists: Some(false), + extra_options: options, + ..Default::default() + }, + ) } #[bun_jsc::host_fn] @@ -260,35 +216,12 @@ pub(crate) fn size(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let options = args.next_eat(); - if matches!(path, crate::node::PathOrFileDescriptor::Fd(_)) { - return Err(global - .throw_invalid_arguments(format_args!("Expected a S3 or path to get size"))); - } - let mut blob = construct_s3_file_internal_store(global, path.path().clone(), options)?; - - S3BlobStatTask::size(global, &mut blob) - } - PathOrBlob::Blob(blob) => Ok(blob.get_size(global)), - } + let error_message = "Expected a S3 or path to get size"; + let mut blob = match parse_s3_path_or_blob(global, &mut args, error_message)? { + PathOrBlob::Blob(blob) => return Ok(blob.get_size(global)), + path => resolve_s3_blob(global, &mut args, path, error_message)?.0, + }; + S3BlobStatTask::size(global, &mut blob) } #[bun_jsc::host_fn] @@ -297,36 +230,12 @@ pub(crate) fn exists(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult // SAFETY: bun_vm() returns the live VM raw ptr. let mut args = bun_jsc::call_frame::ArgumentsSlice::init(global.bun_vm(), arguments.slice()); - // accept a path or a blob - let mut path_or_blob = PathOrBlob::from_js_no_copy(global, &mut args)?; - - if let PathOrBlob::Blob(blob) = &path_or_blob { - if blob.store.get().is_none() - || !matches!( - blob.store.get().as_ref().unwrap().data, - blob::store::Data::S3(_) - ) - { - return Err(global.throw_invalid_arguments(format_args!( - "Expected a S3 or path to check if it exists" - ))); - } - } - - match &mut path_or_blob { - PathOrBlob::Path(path) => { - let options = args.next_eat(); - if matches!(path, crate::node::PathOrFileDescriptor::Fd(_)) { - return Err(global.throw_invalid_arguments(format_args!( - "Expected a S3 or path to check if it exists" - ))); - } - let blob = construct_s3_file_internal_store(global, path.path().clone(), options)?; - - S3BlobStatTask::exists(global, &blob) - } - PathOrBlob::Blob(blob) => blob.get_exists(global, callframe), - } + let error_message = "Expected a S3 or path to check if it exists"; + let blob = match parse_s3_path_or_blob(global, &mut args, error_message)? { + PathOrBlob::Blob(blob) => return blob.get_exists(global, callframe), + path => resolve_s3_blob(global, &mut args, path, error_message)?.0, + }; + S3BlobStatTask::exists(global, &blob) } fn construct_s3_file_internal_store( @@ -358,7 +267,7 @@ pub(crate) fn construct_s3_file_with_s3_credentials_and_options( default_storage_class: Option, default_request_payer: bool, ) -> JsResult { - let aws_options = ::get_credentials_with_options( + let mut aws_options = ::get_credentials_with_options( default_credentials, default_options, options, @@ -368,18 +277,28 @@ pub(crate) fn construct_s3_file_with_s3_credentials_and_options( global, )?; - let mut store = 'brk: { - if aws_options.changed_credentials { - break 'brk blob::Store::init_s3(path, None, aws_options.credentials).expect("oom"); - } else { - // The `Store::S3` field is `Rc` (separate rc - // layer), so we can't share the existing intrusive allocation — - // deep-clone the value instead and let `init_s3` `Rc::new` it. - // PERF: profile if hot once Store.rs migrates - // `Rc` → `IntrusiveRc`. - break 'brk blob::Store::init_s3(path, None, default_credentials.clone()).expect("oom"); - } + let credentials = if aws_options.changed_credentials { + std::mem::take(&mut aws_options.credentials) + } else { + // The `Store::S3` field is `Rc` (separate rc + // layer), so we can't share the existing intrusive allocation — + // deep-clone the value instead and let `init_s3` `Rc::new` it. + // PERF: profile if hot once Store.rs migrates + // `Rc` → `IntrusiveRc`. + default_credentials.clone() }; + let store = blob::Store::init_s3(path, None, credentials).expect("oom"); + finish_s3_blob(global, store, &aws_options, options) +} + +/// Shared construction epilogue: copy the parsed S3 options onto the store, +/// wrap it in a `Blob`, and apply any `options.type` content-type override. +fn finish_s3_blob( + global: &JSGlobalObject, + mut store: Box, + aws_options: &s3::S3CredentialsWithOptions, + options: Option, +) -> JsResult { // store cleanup on early return is handled by Drop store.data.as_s3_mut().options = aws_options.options; store.data.as_s3_mut().acl = aws_options.acl; @@ -437,7 +356,7 @@ pub(crate) fn construct_s3_file_with_s3_credentials( options: Option, existing_credentials: &s3::S3Credentials, ) -> JsResult { - let aws_options = ::get_credentials_with_options( + let mut aws_options = ::get_credentials_with_options( existing_credentials, Default::default(), options, @@ -446,56 +365,9 @@ pub(crate) fn construct_s3_file_with_s3_credentials( false, global, )?; - let mut store = blob::Store::init_s3(path, None, aws_options.credentials).expect("oom"); - // store cleanup on early return is handled by Drop - store.data.as_s3_mut().options = aws_options.options; - store.data.as_s3_mut().acl = aws_options.acl; - store.data.as_s3_mut().storage_class = aws_options.storage_class; - store.data.as_s3_mut().request_payer = aws_options.request_payer; - - let blob = Blob::init_with_store(store, global); - if let Some(opts) = options { - if opts.is_object() { - if let Some(file_type) = opts.get_truthy(global, "type")? { - 'inner: { - if file_type.is_string() { - let str = file_type.to_slice(global)?; - let slice = str.slice(); - if !blob::is_valid_blob_type(slice) { - break 'inner; - } - blob.content_type_was_set.set(true); - // SAFETY: bun_vm() returns the live VM raw ptr. - if let Some(entry) = global.bun_vm().as_mut().mime_type(str.slice()) { - // `MimeType.value` is `Cow<'static, [u8]>`; the - // canonical-table hit (via `Compact::to_mime_type`) is always - // `Borrowed(&'static)`. If a future table source ever yields - // `Owned`, hand the buffer to the blob's allocated-content-type - // path so `Blob::deinit` reclaims it. - match entry.value { - std::borrow::Cow::Borrowed(s) => { - blob.content_type.set(std::ptr::from_ref::<[u8]>(s)); - } - std::borrow::Cow::Owned(v) => { - blob.content_type - .set(bun_core::heap::into_raw(v.into_boxed_slice())); - blob.content_type_allocated.set(true); - } - } - break 'inner; - } - let mut content_type_buf = vec![0u8; slice.len()]; - strings::copy_lowercase(slice, &mut content_type_buf); - blob.content_type.set(bun_core::heap::into_raw( - content_type_buf.into_boxed_slice(), - )); - blob.content_type_allocated.set(true); - } - } - } - } - } - Ok(blob) + let credentials = std::mem::take(&mut aws_options.credentials); + let store = blob::Store::init_s3(path, None, credentials).expect("oom"); + finish_s3_blob(global, store, &aws_options, options) } fn construct_s3_file_internal( @@ -858,35 +730,10 @@ pub(crate) fn stat(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let options = args.next_eat(); - if matches!(path, crate::node::PathOrFileDescriptor::Fd(_)) { - return Err(global - .throw_invalid_arguments(format_args!("Expected a S3 or path to get size"))); - } - let blob = construct_s3_file_internal_store(global, path.path().clone(), options)?; - - S3BlobStatTask::stat(global, &blob) - } - PathOrBlob::Blob(blob) => S3BlobStatTask::stat(global, blob), - } + let error_message = "Expected a S3 or path to get size"; + let path_or_blob = parse_s3_path_or_blob(global, &mut args, error_message)?; + let (blob, _options) = resolve_s3_blob(global, &mut args, path_or_blob, error_message)?; + S3BlobStatTask::stat(global, &blob) } pub(crate) fn construct_internal_js( diff --git a/src/runtime/webcore/blob/copy_file.rs b/src/runtime/webcore/blob/copy_file.rs index 831c84a0bee4..8c17eec1efbc 100644 --- a/src/runtime/webcore/blob/copy_file.rs +++ b/src/runtime/webcore/blob/copy_file.rs @@ -309,6 +309,41 @@ impl<'a> CopyFile<'a> { Ok(()) } + #[cfg(any(target_os = "linux", target_os = "android"))] + fn fallback_read_write( + &mut self, + src_fd: Fd, + dest_fd: Fd, + remain: usize, + unknown_size: bool, + total_written: &mut u64, + ) -> Result<(), bun_core::Error> { + // TODO: this should use non-blocking I/O. + match node_fs::NodeFS::copy_file_using_read_write_loop( + bun_core::ZStr::EMPTY, + bun_core::ZStr::EMPTY, + src_fd, + dest_fd, + if unknown_size { 0 } else { remain }, + total_written, + ) { + bun_sys::Result::Err(err) => { + self.system_error = Some(err.to_system_error()); + Err(bun_core::errno_to_zig_err(err.errno as i32)) + } + bun_sys::Result::Ok(()) => { + // SAFETY: dest_fd is a valid open fd; raw ftruncate(2). + let _ = unsafe { + libc::ftruncate( + dest_fd.native(), + i64::try_from(*total_written).expect("int cast"), + ) + }; + Ok(()) + } + } + } + #[cfg(any(target_os = "linux", target_os = "android"))] pub fn do_copy_file_range( &mut self, @@ -343,29 +378,13 @@ impl<'a> CopyFile<'a> { // If they can't use copy_file_range, they probably also can't // use sendfile() or splice() if !bun_sys::copy_file::can_use_copy_file_range_syscall() { - match node_fs::NodeFS::copy_file_using_read_write_loop( - bun_core::ZStr::EMPTY, - bun_core::ZStr::EMPTY, + return self.fallback_read_write( src_fd, dest_fd, - if unknown_size { 0 } else { remain }, + remain, + unknown_size, &mut total_written, - ) { - bun_sys::Result::Err(err) => { - self.system_error = Some(err.to_system_error()); - return Err(bun_core::errno_to_zig_err(err.errno as i32)); - } - bun_sys::Result::Ok(()) => { - // SAFETY: dest_fd is a valid open fd; raw ftruncate(2). - let _ = unsafe { - libc::ftruncate( - dest_fd.native(), - i64::try_from(total_written).expect("int cast"), - ) - }; - return Ok(()); - } - } + ); } loop { @@ -417,30 +436,13 @@ impl<'a> CopyFile<'a> { // NOSYS: syscall not available // OPNOTSUPP: filesystem doesn't support this operation bun_sys::E::ENOSYS | bun_sys::E::EXDEV | bun_sys::E::ENOTSUP => { - // TODO: this should use non-blocking I/O. - match node_fs::NodeFS::copy_file_using_read_write_loop( - bun_core::ZStr::EMPTY, - bun_core::ZStr::EMPTY, + return self.fallback_read_write( src_fd, dest_fd, - if unknown_size { 0 } else { remain }, + remain, + unknown_size, &mut total_written, - ) { - bun_sys::Result::Err(err) => { - self.system_error = Some(err.to_system_error()); - return Err(bun_core::errno_to_zig_err(err.errno as i32)); - } - bun_sys::Result::Ok(()) => { - // SAFETY: dest_fd is a valid open fd; raw ftruncate(2). - let _ = unsafe { - libc::ftruncate( - dest_fd.native(), - i64::try_from(total_written).expect("int cast"), - ) - }; - return Ok(()); - } - } + ); } // EINVAL: eCryptfs and other filesystems may not support copy_file_range. @@ -474,30 +476,13 @@ impl<'a> CopyFile<'a> { // incompatible with the chosen syscall, fall back // to a read/write loop if total_written == 0 { - // TODO: this should use non-blocking I/O. - match node_fs::NodeFS::copy_file_using_read_write_loop( - bun_core::ZStr::EMPTY, - bun_core::ZStr::EMPTY, + return self.fallback_read_write( src_fd, dest_fd, - if unknown_size { 0 } else { remain }, + remain, + unknown_size, &mut total_written, - ) { - bun_sys::Result::Err(err) => { - self.system_error = Some(err.to_system_error()); - return Err(bun_core::errno_to_zig_err(err.errno as i32)); - } - bun_sys::Result::Ok(()) => { - // SAFETY: dest_fd is a valid open fd; raw ftruncate(2). - let _ = unsafe { - libc::ftruncate( - dest_fd.native(), - i64::try_from(total_written).expect("int cast"), - ) - }; - return Ok(()); - } - } + ); } self.system_error = Some( diff --git a/src/runtime/webcore/blob/read_file.rs b/src/runtime/webcore/blob/read_file.rs index a79cdc061347..7ef422ec6edb 100644 --- a/src/runtime/webcore/blob/read_file.rs +++ b/src/runtime/webcore/blob/read_file.rs @@ -204,6 +204,7 @@ pub struct ReadFile { } bun_threading::intrusive_work_task!(ReadFile, task); +bun_io::intrusive_io_request!(ReadFile, io_request); // The default methods on the FileOpener/FileCloser traits provide the bodies. impl FileOpener for ReadFile { @@ -240,81 +241,7 @@ impl FileOpener for ReadFile { } } -impl FileCloser for ReadFile { - const IO_TAG: bun_io::Tag = bun_io::Tag::ReadFile; - fn opened_fd(&self) -> Fd { - self.opened_fd - } - fn set_opened_fd(&mut self, fd: Fd) { - self.opened_fd = fd; - } - fn close_after_io(&self) -> bool { - self.close_after_io - } - fn set_close_after_io(&mut self, v: bool) { - self.close_after_io = v; - } - fn state(&self) -> &AtomicU8 { - &self.state - } - fn io_request(&mut self) -> Option<&mut bun_io::Request> { - Some(&mut self.io_request) - } - fn io_poll(&mut self) -> &mut bun_io::Poll { - &mut self.io_poll - } - fn task(&mut self) -> &mut bun_jsc::WorkPoolTask { - &mut self.task - } - fn update(&mut self) { - ReadFile::update(self) - } - #[cfg(windows)] - fn loop_(&self) -> *mut bun_libuv_sys::uv_loop_t { - unreachable!() - } - - fn schedule_close(request: &mut bun_io::Request) -> bun_io::Action<'_> { - // SAFETY: request is &mut self.io_request (intrusive); recover parent. - let this: &mut ReadFile = unsafe { - &mut *(bun_core::from_field_ptr!( - ReadFile, - io_request, - std::ptr::from_mut::(request) - )) - }; - fn on_done(ctx: *mut ()) { - // SAFETY: ctx is `self as *mut ReadFile` set below. - let this = unsafe { bun_ptr::callback_ctx::(ctx.cast()) }; - ::on_io_request_closed(this); - } - // reshaped for borrowck — compute the parent raw pointer - // before mutably borrowing `io_poll` so the two borrows do not overlap. - let ctx = std::ptr::from_mut::(this).cast::<()>(); - let fd = this.opened_fd; - io::Action::Close(io::CloseAction { - fd, - poll: &mut this.io_poll, - ctx, - tag: ::IO_TAG, - on_done, - }) - } - - // `FileCloser` fixes `on_close_io_request` to take `*mut WorkPoolTask`; - // the trait method cannot be marked `unsafe fn`, so the lint is - // unsatisfiable here. The pointer is the intrusive `&mut self.task` set - // in `on_io_request_closed` and is guaranteed live. - #[allow(clippy::not_unsafe_ptr_arg_deref)] - fn on_close_io_request(task: *mut bun_jsc::WorkPoolTask) { - // SAFETY: only reached via `WorkPoolTask::callback` with `task` = - // `&mut self.task` (intrusive) registered in `on_io_request_closed`; - // recover parent. - let this = unsafe { &mut *ReadFile::from_task_ptr(task) }; - this.close_after_io = false; - ReadFile::update(this); - } -} +crate::webcore::blob::impl_file_closer!(ReadFile); impl ReadFile { pub fn update(&mut self) { diff --git a/src/runtime/webcore/blob/write_file.rs b/src/runtime/webcore/blob/write_file.rs index f49196680e5a..59f33e8a9847 100644 --- a/src/runtime/webcore/blob/write_file.rs +++ b/src/runtime/webcore/blob/write_file.rs @@ -13,9 +13,10 @@ use bun_jsc::{self as jsc, JSGlobalObject, JSPromise, JSValue, JsTerminated, Sys use bun_sys::{self as sys, Fd}; use bun_threading::{IntrusiveWorkTask as _, WorkPool, WorkPoolTask}; +#[cfg(not(windows))] +use crate::webcore::blob::FileCloser; use crate::webcore::blob::{ - self, Blob, ClosingState, FileCloser, FileOpener, MkdirpTarget, Retry, SizeType, - mkdir_if_not_exists, + self, Blob, ClosingState, FileOpener, MkdirpTarget, Retry, SizeType, mkdir_if_not_exists, }; use crate::webcore::body; @@ -152,75 +153,7 @@ impl MkdirpTarget for WriteFile { } } -impl FileCloser for WriteFile { - const IO_TAG: io::Tag = io::Tag::WriteFile; - fn opened_fd(&self) -> Fd { - self.opened_fd - } - fn set_opened_fd(&mut self, fd: Fd) { - self.opened_fd = fd; - } - fn close_after_io(&self) -> bool { - self.close_after_io - } - fn set_close_after_io(&mut self, v: bool) { - self.close_after_io = v; - } - fn state(&self) -> &AtomicU8 { - &self.state - } - fn io_request(&mut self) -> Option<&mut io::Request> { - Some(&mut self.io_request) - } - fn io_poll(&mut self) -> &mut io::Poll { - &mut self.io_poll - } - fn task(&mut self) -> &mut bun_jsc::WorkPoolTask { - &mut self.task - } - fn update(&mut self) { - WriteFile::update(self) - } - #[cfg(windows)] - fn loop_(&self) -> *mut bun_libuv_sys::uv_loop_t { - unreachable!() - } - - fn schedule_close(request: &mut io::Request) -> io::Action<'_> { - // SAFETY: request is &mut self.io_request (intrusive); recover parent. - let this = unsafe { &mut *WriteFile::from_io_request(std::ptr::from_mut(request)) }; - fn on_done(ctx: *mut ()) { - // SAFETY: ctx is `self as *mut WriteFile` set below. - let this = unsafe { bun_ptr::callback_ctx::(ctx.cast()) }; - ::on_io_request_closed(this); - } - // reshaped for borrowck — compute the parent raw pointer - // before mutably borrowing `io_poll` so the two borrows do not overlap. - let ctx = std::ptr::from_mut::(this).cast::<()>(); - let fd = this.opened_fd; - io::Action::Close(io::CloseAction { - fd, - poll: &mut this.io_poll, - ctx, - tag: ::IO_TAG, - on_done, - }) - } - - // `FileCloser` fixes `on_close_io_request` to take `*mut WorkPoolTask`; - // the trait method cannot be marked `unsafe fn`, so the lint is - // unsatisfiable here. The pointer is the intrusive `&mut self.task` set - // in `on_io_request_closed` and is guaranteed live. - #[allow(clippy::not_unsafe_ptr_arg_deref)] - fn on_close_io_request(task: *mut bun_jsc::WorkPoolTask) { - // SAFETY: only reached via `WorkPoolTask::callback` with `task` = - // `&mut self.task` (intrusive) registered in `on_io_request_closed`; - // recover parent. - let this = unsafe { &mut *WriteFile::from_task_ptr(task) }; - this.close_after_io = false; - WriteFile::update(this); - } -} +crate::webcore::blob::impl_file_closer!(WriteFile); impl WriteFile { pub const IO_TAG: io::Tag = io::Tag::WriteFile; diff --git a/src/runtime/webcore/fetch.rs b/src/runtime/webcore/fetch.rs index e1e4441351dd..9115c5467e63 100644 --- a/src/runtime/webcore/fetch.rs +++ b/src/runtime/webcore/fetch.rs @@ -54,7 +54,7 @@ use bun_core::{String as BunString, Tag as BunStringTag, ZigStringSlice}; use bun_http::{self as http, FetchRedirect, Headers, HeadersExt as _, MimeType}; use bun_http_jsc::method_jsc; use bun_http_types::Method::Method; -use bun_jsc::{HTTPHeaderName, StringJsc as _, SysErrorJsc as _}; +use bun_jsc::{HTTPHeaderName, StringJsc as _, SysErrorJsc as _, UrlJsc as _}; use bun_paths::{self, PathBuffer}; use bun_sys::FdExt as _; // `FromJsEnum for FetchRedirect` lives in bun_http_jsc; importing the impl crate diff --git a/src/runtime/webcore/streams.rs b/src/runtime/webcore/streams.rs index dedc030ef928..2cc900d6e982 100644 --- a/src/runtime/webcore/streams.rs +++ b/src/runtime/webcore/streams.rs @@ -2002,6 +2002,56 @@ impl HTTPServerWritable { crate::impl_sink_handler!([const SSL: bool, const HTTP3: bool] HTTPServerWritable); +/// Emits the standard `JsSinkType` forwarders (`memory_cost`, `write_bytes`, +/// `write_utf16`, `write_latin1`, `end`, `flush`, `flush_from_js`, `start`) +/// inside an `impl JsSinkType for T` block. `Self::name` prefers the inherent +/// item over the trait item being defined, so the forward never recurses +/// (same rule as `impl_sink_handler!`). The genuinely divergent items +/// (`finalize`, `construct`, `end_from_js`, `signal`, `done`, the `HAS_*` +/// consts) stay hand-written per type. +#[macro_export] +macro_rules! impl_js_sink_forwarders { + () => { + fn memory_cost(&self) -> usize { + Self::memory_cost(self) + } + fn write_bytes( + &mut self, + data: &$crate::webcore::streams::Result, + ) -> $crate::webcore::streams::result::Writable { + Self::write(self, data) + } + fn write_utf16( + &mut self, + data: &$crate::webcore::streams::Result, + ) -> $crate::webcore::streams::result::Writable { + Self::write_utf16(self, data) + } + fn write_latin1( + &mut self, + data: &$crate::webcore::streams::Result, + ) -> $crate::webcore::streams::result::Writable { + Self::write_latin1(self, data) + } + fn end(&mut self, err: ::core::option::Option<::bun_sys::Error>) -> ::bun_sys::Result<()> { + Self::end(self, err) + } + fn flush(&mut self) -> ::bun_sys::Result<()> { + Self::flush(self) + } + fn flush_from_js( + &mut self, + global: &::bun_jsc::JSGlobalObject, + wait: bool, + ) -> ::bun_sys::Result<::bun_jsc::JSValue> { + Self::flush_from_js(self, global, wait) + } + fn start(&mut self, config: $crate::webcore::streams::Start) -> ::bun_sys::Result<()> { + Self::start(self, &config) + } + }; +} + // `JsSinkType` impl: routes the codegen `${name}__{construct,write,end,flush, // start,getInternalFd,memoryCost}` thunks (via `JSSink::::js_*`) into // the inherent streaming methods above. Mirrors `Sink.JSSink(@This(), name)`. @@ -2020,36 +2070,14 @@ impl crate::webcore::sink::JsSinkType StartTag::HTTPResponseSink }); - fn memory_cost(&self) -> usize { - Self::memory_cost(self) - } + crate::impl_js_sink_forwarders!(); + fn finalize(&mut self) { Self::finalize(self) } - fn write_bytes(&mut self, data: &StreamResult) -> Writable { - Self::write(self, data) - } - fn write_utf16(&mut self, data: &StreamResult) -> Writable { - Self::write_utf16(self, data) - } - fn write_latin1(&mut self, data: &StreamResult) -> Writable { - Self::write_latin1(self, data) - } - fn end(&mut self, err: Option) -> bun_sys::Result<()> { - Self::end(self, err) - } fn end_from_js(&mut self, global: &JSGlobalObject) -> bun_sys::Result { Self::end_from_js(self, global) } - fn flush(&mut self) -> bun_sys::Result<()> { - Self::flush(self) - } - fn flush_from_js(&mut self, global: &JSGlobalObject, wait: bool) -> bun_sys::Result { - Self::flush_from_js(self, global, wait) - } - fn start(&mut self, config: Start) -> bun_sys::Result<()> { - Self::start(self, &config) - } fn signal(&mut self) -> Option<&mut Signal> { Some(&mut self.signal) } @@ -2378,36 +2406,14 @@ impl crate::webcore::sink::JsSinkType for NetworkSink { const HAS_FLUSH_FROM_JS: bool = true; const START_TAG: Option = Some(StartTag::NetworkSink); - fn memory_cost(&self) -> usize { - Self::memory_cost(self) - } + crate::impl_js_sink_forwarders!(); + fn finalize(&mut self) { Self::finalize(self) } - fn write_bytes(&mut self, data: &StreamResult) -> Writable { - Self::write(self, data) - } - fn write_utf16(&mut self, data: &StreamResult) -> Writable { - Self::write_utf16(self, data) - } - fn write_latin1(&mut self, data: &StreamResult) -> Writable { - Self::write_latin1(self, data) - } - fn end(&mut self, err: Option) -> bun_sys::Result<()> { - Self::end(self, err) - } fn end_from_js(&mut self, global: &JSGlobalObject) -> bun_sys::Result { Self::end_from_js(self, global) } - fn flush(&mut self) -> bun_sys::Result<()> { - Self::flush(self) - } - fn flush_from_js(&mut self, global: &JSGlobalObject, wait: bool) -> bun_sys::Result { - Self::flush_from_js(self, global, wait) - } - fn start(&mut self, config: Start) -> bun_sys::Result<()> { - Self::start(self, &config) - } fn signal(&mut self) -> Option<&mut Signal> { Some(&mut self.signal) } diff --git a/src/semver/Version.rs b/src/semver/Version.rs index 8431e1c86e1c..7ad5681473af 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 b7e87959f7ac..2787df0ead99 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<(), bun_core::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 2d58dd7c90bc..35fcc207af0c 100644 --- a/src/sourcemap/lib.rs +++ b/src/sourcemap/lib.rs @@ -1352,22 +1352,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/sql/lib.rs b/src/sql/lib.rs index 23d7875262ba..f3ec0b3b5e46 100644 --- a/src/sql/lib.rs +++ b/src/sql/lib.rs @@ -7,13 +7,20 @@ pub mod shared { pub mod connection_flags; #[path = "Data.rs"] pub mod data; + #[path = "QueryStatus.rs"] + pub mod query_status; #[path = "SQLQueryResultMode.rs"] pub mod sql_query_result_mode; + #[path = "StackReader.rs"] + pub mod stack_reader; + #[path = "StatementStatus.rs"] + pub mod statement_status; pub use column_identifier::ColumnIdentifier; pub use connection_flags::ConnectionFlags; pub use data::Data; pub use sql_query_result_mode::SQLQueryResultMode; + pub use stack_reader::StackReader; } pub mod mysql { @@ -31,8 +38,6 @@ pub mod mysql { pub mod mysql_request; #[path = "MySQLTypes.rs"] pub mod mysql_types; - #[path = "QueryStatus.rs"] - pub mod query_status; #[path = "SSLMode.rs"] pub mod ssl_mode; #[path = "StatusFlags.rs"] @@ -105,7 +110,7 @@ pub mod mysql { pub use handshake_response41::HandshakeResponse41; pub use handshake_v10::HandshakeV10; pub use local_infile_request::LocalInfileRequest; - pub use new_reader::{Decode, NewReader, NewReaderOf, ReadableInt, ReaderContext}; + pub use new_reader::{Decode, NewReader, ReadableInt, ReaderContext}; pub use new_writer::{NewWriter, NewWriterWrap, Packet, WriterContext, write_wrap}; pub use ok_packet::OKPacket; pub use packet_header::PacketHeader; @@ -118,11 +123,12 @@ pub mod mysql { pub use crate::mysql::mysql_types::FieldType; } + pub use crate::shared::query_status; + pub use crate::shared::query_status::Status as QueryStatus; pub use auth_method::AuthMethod; pub use capabilities::Capabilities; pub use connection_state::ConnectionState; pub use mysql_query_result::MySQLQueryResult; - pub use query_status::Status as QueryStatus; pub use ssl_mode::SSLMode; pub use status_flags::{StatusFlag, StatusFlags}; pub use tls_status::TLSStatus; diff --git a/src/sql/mysql/Capabilities.rs b/src/sql/mysql/Capabilities.rs index 7d91fc866d6e..f93e7794cc93 100644 --- a/src/sql/mysql/Capabilities.rs +++ b/src/sql/mysql/Capabilities.rs @@ -1,84 +1,94 @@ // MySQL capability flags // // Modeled as a plain struct of `bool` fields (NOT a `packed struct(u32)`-style -// bitfield), with explicit bit-value constants and -// hand-unrolled field loops for `to_int`/`from_int`/`Display`. Field names stay -// SCREAMING_SNAKE_CASE because `Display` emits them verbatim. +// bitfield). The `capabilities!` macro takes the single (field, bit) list and +// generates the pub-field struct plus `to_int`/`from_int`/`Display`. +// Field names stay SCREAMING_SNAKE_CASE because `Display` emits them verbatim. // (non_snake_case / non_upper_case_globals allowed at crate root.) use core::fmt; -#[derive(Default, Clone, Copy, PartialEq, Eq)] -pub struct Capabilities { - pub CLIENT_LONG_PASSWORD: bool, - pub CLIENT_FOUND_ROWS: bool, - pub CLIENT_LONG_FLAG: bool, - pub CLIENT_CONNECT_WITH_DB: bool, - pub CLIENT_NO_SCHEMA: bool, - pub CLIENT_COMPRESS: bool, - pub CLIENT_ODBC: bool, - pub CLIENT_LOCAL_FILES: bool, - pub CLIENT_IGNORE_SPACE: bool, - pub CLIENT_PROTOCOL_41: bool, - pub CLIENT_INTERACTIVE: bool, - pub CLIENT_SSL: bool, - pub CLIENT_IGNORE_SIGPIPE: bool, - pub CLIENT_TRANSACTIONS: bool, - pub CLIENT_RESERVED: bool, - pub CLIENT_SECURE_CONNECTION: bool, - pub CLIENT_MULTI_STATEMENTS: bool, - pub CLIENT_MULTI_RESULTS: bool, - pub CLIENT_PS_MULTI_RESULTS: bool, - pub CLIENT_PLUGIN_AUTH: bool, - pub CLIENT_CONNECT_ATTRS: bool, - pub CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA: bool, - pub CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS: bool, - pub CLIENT_SESSION_TRACK: bool, - pub CLIENT_DEPRECATE_EOF: bool, - pub CLIENT_OPTIONAL_RESULTSET_METADATA: bool, - pub CLIENT_ZSTD_COMPRESSION_ALGORITHM: bool, - pub CLIENT_QUERY_ATTRIBUTES: bool, - pub MULTI_FACTOR_AUTHENTICATION: bool, - pub CLIENT_CAPABILITY_EXTENSION: bool, - pub CLIENT_SSL_VERIFY_SERVER_CERT: bool, - pub CLIENT_REMEMBER_OPTIONS: bool, +macro_rules! capabilities { + ($($field:ident = $bit:expr,)*) => { + #[derive(Default, Clone, Copy, PartialEq, Eq)] + pub struct Capabilities { + $(pub $field: bool,)* + } + + impl Capabilities { + pub fn to_int(self) -> u32 { + let mut value: u32 = 0; + $( + if self.$field { + value |= $bit; + } + )* + value + } + + pub fn from_int(flags: u32) -> Capabilities { + Capabilities { + $($field: (flags & $bit) != 0,)* + } + } + } + + impl fmt::Display for Capabilities { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut first = true; + $( + if self.$field { + if !first { + f.write_str(", ")?; + } + first = false; + f.write_str(stringify!($field))?; + } + )* + let _ = first; + Ok(()) + } + } + }; } -impl Capabilities { - // Constants with correct shift values from MySQL protocol - const _CLIENT_LONG_PASSWORD: u32 = 1; // 1 << 0 - const _CLIENT_FOUND_ROWS: u32 = 2; // 1 << 1 - const _CLIENT_LONG_FLAG: u32 = 4; // 1 << 2 - const _CLIENT_CONNECT_WITH_DB: u32 = 8; // 1 << 3 - const _CLIENT_NO_SCHEMA: u32 = 16; // 1 << 4 - const _CLIENT_COMPRESS: u32 = 32; // 1 << 5 - const _CLIENT_ODBC: u32 = 64; // 1 << 6 - const _CLIENT_LOCAL_FILES: u32 = 128; // 1 << 7 - const _CLIENT_IGNORE_SPACE: u32 = 256; // 1 << 8 - const _CLIENT_PROTOCOL_41: u32 = 512; // 1 << 9 - const _CLIENT_INTERACTIVE: u32 = 1024; // 1 << 10 - const _CLIENT_SSL: u32 = 2048; // 1 << 11 - const _CLIENT_IGNORE_SIGPIPE: u32 = 4096; // 1 << 12 - const _CLIENT_TRANSACTIONS: u32 = 8192; // 1 << 13 - const _CLIENT_RESERVED: u32 = 16384; // 1 << 14 - const _CLIENT_SECURE_CONNECTION: u32 = 32768; // 1 << 15 - const _CLIENT_MULTI_STATEMENTS: u32 = 65536; // 1 << 16 - const _CLIENT_MULTI_RESULTS: u32 = 131072; // 1 << 17 - const _CLIENT_PS_MULTI_RESULTS: u32 = 262144; // 1 << 18 - const _CLIENT_PLUGIN_AUTH: u32 = 524288; // 1 << 19 - const _CLIENT_CONNECT_ATTRS: u32 = 1048576; // 1 << 20 - const _CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA: u32 = 2097152; // 1 << 21 - const _CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS: u32 = 4194304; // 1 << 22 - const _CLIENT_SESSION_TRACK: u32 = 8388608; // 1 << 23 - const _CLIENT_DEPRECATE_EOF: u32 = 16777216; // 1 << 24 - const _CLIENT_OPTIONAL_RESULTSET_METADATA: u32 = 33554432; // 1 << 25 - const _CLIENT_ZSTD_COMPRESSION_ALGORITHM: u32 = 67108864; // 1 << 26 - const _CLIENT_QUERY_ATTRIBUTES: u32 = 134217728; // 1 << 27 - const _MULTI_FACTOR_AUTHENTICATION: u32 = 268435456; // 1 << 28 - const _CLIENT_CAPABILITY_EXTENSION: u32 = 536870912; // 1 << 29 - const _CLIENT_SSL_VERIFY_SERVER_CERT: u32 = 1073741824; // 1 << 30 - const _CLIENT_REMEMBER_OPTIONS: u32 = 2147483648; // 1 << 31 +// Bit values from the MySQL protocol. +capabilities! { + CLIENT_LONG_PASSWORD = 1 << 0, + CLIENT_FOUND_ROWS = 1 << 1, + CLIENT_LONG_FLAG = 1 << 2, + CLIENT_CONNECT_WITH_DB = 1 << 3, + CLIENT_NO_SCHEMA = 1 << 4, + CLIENT_COMPRESS = 1 << 5, + CLIENT_ODBC = 1 << 6, + CLIENT_LOCAL_FILES = 1 << 7, + CLIENT_IGNORE_SPACE = 1 << 8, + CLIENT_PROTOCOL_41 = 1 << 9, + CLIENT_INTERACTIVE = 1 << 10, + CLIENT_SSL = 1 << 11, + CLIENT_IGNORE_SIGPIPE = 1 << 12, + CLIENT_TRANSACTIONS = 1 << 13, + CLIENT_RESERVED = 1 << 14, + CLIENT_SECURE_CONNECTION = 1 << 15, + CLIENT_MULTI_STATEMENTS = 1 << 16, + CLIENT_MULTI_RESULTS = 1 << 17, + CLIENT_PS_MULTI_RESULTS = 1 << 18, + CLIENT_PLUGIN_AUTH = 1 << 19, + CLIENT_CONNECT_ATTRS = 1 << 20, + CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA = 1 << 21, + CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS = 1 << 22, + CLIENT_SESSION_TRACK = 1 << 23, + CLIENT_DEPRECATE_EOF = 1 << 24, + CLIENT_OPTIONAL_RESULTSET_METADATA = 1 << 25, + CLIENT_ZSTD_COMPRESSION_ALGORITHM = 1 << 26, + CLIENT_QUERY_ATTRIBUTES = 1 << 27, + MULTI_FACTOR_AUTHENTICATION = 1 << 28, + CLIENT_CAPABILITY_EXTENSION = 1 << 29, + CLIENT_SSL_VERIFY_SERVER_CERT = 1 << 30, + CLIENT_REMEMBER_OPTIONS = 1 << 31, +} +impl Capabilities { pub fn reject(&mut self) { self.CLIENT_ZSTD_COMPRESSION_ALGORITHM = false; self.MULTI_FACTOR_AUTHENTICATION = false; @@ -95,154 +105,6 @@ impl Capabilities { self.CLIENT_QUERY_ATTRIBUTES = false; } - pub fn to_int(self) -> u32 { - let mut value: u32 = 0; - - // Keep in sync with the Capabilities field list above. - if self.CLIENT_LONG_PASSWORD { - value |= Self::_CLIENT_LONG_PASSWORD; - } - if self.CLIENT_FOUND_ROWS { - value |= Self::_CLIENT_FOUND_ROWS; - } - if self.CLIENT_LONG_FLAG { - value |= Self::_CLIENT_LONG_FLAG; - } - if self.CLIENT_CONNECT_WITH_DB { - value |= Self::_CLIENT_CONNECT_WITH_DB; - } - if self.CLIENT_NO_SCHEMA { - value |= Self::_CLIENT_NO_SCHEMA; - } - if self.CLIENT_COMPRESS { - value |= Self::_CLIENT_COMPRESS; - } - if self.CLIENT_ODBC { - value |= Self::_CLIENT_ODBC; - } - if self.CLIENT_LOCAL_FILES { - value |= Self::_CLIENT_LOCAL_FILES; - } - if self.CLIENT_IGNORE_SPACE { - value |= Self::_CLIENT_IGNORE_SPACE; - } - if self.CLIENT_PROTOCOL_41 { - value |= Self::_CLIENT_PROTOCOL_41; - } - if self.CLIENT_INTERACTIVE { - value |= Self::_CLIENT_INTERACTIVE; - } - if self.CLIENT_SSL { - value |= Self::_CLIENT_SSL; - } - if self.CLIENT_IGNORE_SIGPIPE { - value |= Self::_CLIENT_IGNORE_SIGPIPE; - } - if self.CLIENT_TRANSACTIONS { - value |= Self::_CLIENT_TRANSACTIONS; - } - if self.CLIENT_RESERVED { - value |= Self::_CLIENT_RESERVED; - } - if self.CLIENT_SECURE_CONNECTION { - value |= Self::_CLIENT_SECURE_CONNECTION; - } - if self.CLIENT_MULTI_STATEMENTS { - value |= Self::_CLIENT_MULTI_STATEMENTS; - } - if self.CLIENT_MULTI_RESULTS { - value |= Self::_CLIENT_MULTI_RESULTS; - } - if self.CLIENT_PS_MULTI_RESULTS { - value |= Self::_CLIENT_PS_MULTI_RESULTS; - } - if self.CLIENT_PLUGIN_AUTH { - value |= Self::_CLIENT_PLUGIN_AUTH; - } - if self.CLIENT_CONNECT_ATTRS { - value |= Self::_CLIENT_CONNECT_ATTRS; - } - if self.CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA { - value |= Self::_CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA; - } - if self.CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS { - value |= Self::_CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS; - } - if self.CLIENT_SESSION_TRACK { - value |= Self::_CLIENT_SESSION_TRACK; - } - if self.CLIENT_DEPRECATE_EOF { - value |= Self::_CLIENT_DEPRECATE_EOF; - } - if self.CLIENT_OPTIONAL_RESULTSET_METADATA { - value |= Self::_CLIENT_OPTIONAL_RESULTSET_METADATA; - } - if self.CLIENT_ZSTD_COMPRESSION_ALGORITHM { - value |= Self::_CLIENT_ZSTD_COMPRESSION_ALGORITHM; - } - if self.CLIENT_QUERY_ATTRIBUTES { - value |= Self::_CLIENT_QUERY_ATTRIBUTES; - } - if self.MULTI_FACTOR_AUTHENTICATION { - value |= Self::_MULTI_FACTOR_AUTHENTICATION; - } - if self.CLIENT_CAPABILITY_EXTENSION { - value |= Self::_CLIENT_CAPABILITY_EXTENSION; - } - if self.CLIENT_SSL_VERIFY_SERVER_CERT { - value |= Self::_CLIENT_SSL_VERIFY_SERVER_CERT; - } - if self.CLIENT_REMEMBER_OPTIONS { - value |= Self::_CLIENT_REMEMBER_OPTIONS; - } - - value - } - - pub fn from_int(flags: u32) -> Capabilities { - // Keep in sync with the Capabilities field list above. - Capabilities { - CLIENT_LONG_PASSWORD: (Self::_CLIENT_LONG_PASSWORD & flags) != 0, - CLIENT_FOUND_ROWS: (Self::_CLIENT_FOUND_ROWS & flags) != 0, - CLIENT_LONG_FLAG: (Self::_CLIENT_LONG_FLAG & flags) != 0, - CLIENT_CONNECT_WITH_DB: (Self::_CLIENT_CONNECT_WITH_DB & flags) != 0, - CLIENT_NO_SCHEMA: (Self::_CLIENT_NO_SCHEMA & flags) != 0, - CLIENT_COMPRESS: (Self::_CLIENT_COMPRESS & flags) != 0, - CLIENT_ODBC: (Self::_CLIENT_ODBC & flags) != 0, - CLIENT_LOCAL_FILES: (Self::_CLIENT_LOCAL_FILES & flags) != 0, - CLIENT_IGNORE_SPACE: (Self::_CLIENT_IGNORE_SPACE & flags) != 0, - CLIENT_PROTOCOL_41: (Self::_CLIENT_PROTOCOL_41 & flags) != 0, - CLIENT_INTERACTIVE: (Self::_CLIENT_INTERACTIVE & flags) != 0, - CLIENT_SSL: (Self::_CLIENT_SSL & flags) != 0, - CLIENT_IGNORE_SIGPIPE: (Self::_CLIENT_IGNORE_SIGPIPE & flags) != 0, - CLIENT_TRANSACTIONS: (Self::_CLIENT_TRANSACTIONS & flags) != 0, - CLIENT_RESERVED: (Self::_CLIENT_RESERVED & flags) != 0, - CLIENT_SECURE_CONNECTION: (Self::_CLIENT_SECURE_CONNECTION & flags) != 0, - CLIENT_MULTI_STATEMENTS: (Self::_CLIENT_MULTI_STATEMENTS & flags) != 0, - CLIENT_MULTI_RESULTS: (Self::_CLIENT_MULTI_RESULTS & flags) != 0, - CLIENT_PS_MULTI_RESULTS: (Self::_CLIENT_PS_MULTI_RESULTS & flags) != 0, - CLIENT_PLUGIN_AUTH: (Self::_CLIENT_PLUGIN_AUTH & flags) != 0, - CLIENT_CONNECT_ATTRS: (Self::_CLIENT_CONNECT_ATTRS & flags) != 0, - CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA: (Self::_CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA - & flags) - != 0, - CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS: (Self::_CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS - & flags) - != 0, - CLIENT_SESSION_TRACK: (Self::_CLIENT_SESSION_TRACK & flags) != 0, - CLIENT_DEPRECATE_EOF: (Self::_CLIENT_DEPRECATE_EOF & flags) != 0, - CLIENT_OPTIONAL_RESULTSET_METADATA: (Self::_CLIENT_OPTIONAL_RESULTSET_METADATA & flags) - != 0, - CLIENT_ZSTD_COMPRESSION_ALGORITHM: (Self::_CLIENT_ZSTD_COMPRESSION_ALGORITHM & flags) - != 0, - CLIENT_QUERY_ATTRIBUTES: (Self::_CLIENT_QUERY_ATTRIBUTES & flags) != 0, - MULTI_FACTOR_AUTHENTICATION: (Self::_MULTI_FACTOR_AUTHENTICATION & flags) != 0, - CLIENT_CAPABILITY_EXTENSION: (Self::_CLIENT_CAPABILITY_EXTENSION & flags) != 0, - CLIENT_SSL_VERIFY_SERVER_CERT: (Self::_CLIENT_SSL_VERIFY_SERVER_CERT & flags) != 0, - CLIENT_REMEMBER_OPTIONS: (Self::_CLIENT_REMEMBER_OPTIONS & flags) != 0, - } - } - /// Returns the intersection of two capability sets (AND). /// Per MySQL protocol, the client should only request capabilities /// that the server also advertises. @@ -264,55 +126,3 @@ impl Capabilities { } } } - -impl fmt::Display for Capabilities { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut first = true; - // Keep in sync with the Capabilities field list above. - macro_rules! emit { - ($field:ident) => { - if self.$field { - if !first { - f.write_str(", ")?; - } - first = false; - f.write_str(stringify!($field))?; - } - }; - } - emit!(CLIENT_LONG_PASSWORD); - emit!(CLIENT_FOUND_ROWS); - emit!(CLIENT_LONG_FLAG); - emit!(CLIENT_CONNECT_WITH_DB); - emit!(CLIENT_NO_SCHEMA); - emit!(CLIENT_COMPRESS); - emit!(CLIENT_ODBC); - emit!(CLIENT_LOCAL_FILES); - emit!(CLIENT_IGNORE_SPACE); - emit!(CLIENT_PROTOCOL_41); - emit!(CLIENT_INTERACTIVE); - emit!(CLIENT_SSL); - emit!(CLIENT_IGNORE_SIGPIPE); - emit!(CLIENT_TRANSACTIONS); - emit!(CLIENT_RESERVED); - emit!(CLIENT_SECURE_CONNECTION); - emit!(CLIENT_MULTI_STATEMENTS); - emit!(CLIENT_MULTI_RESULTS); - emit!(CLIENT_PS_MULTI_RESULTS); - emit!(CLIENT_PLUGIN_AUTH); - emit!(CLIENT_CONNECT_ATTRS); - emit!(CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA); - emit!(CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS); - emit!(CLIENT_SESSION_TRACK); - emit!(CLIENT_DEPRECATE_EOF); - emit!(CLIENT_OPTIONAL_RESULTSET_METADATA); - emit!(CLIENT_ZSTD_COMPRESSION_ALGORITHM); - emit!(CLIENT_QUERY_ATTRIBUTES); - emit!(MULTI_FACTOR_AUTHENTICATION); - emit!(CLIENT_CAPABILITY_EXTENSION); - emit!(CLIENT_SSL_VERIFY_SERVER_CERT); - emit!(CLIENT_REMEMBER_OPTIONS); - let _ = first; - Ok(()) - } -} diff --git a/src/sql/mysql/protocol/NewReader.rs b/src/sql/mysql/protocol/NewReader.rs index f4790d47c6af..81e5c1d96e95 100644 --- a/src/sql/mysql/protocol/NewReader.rs +++ b/src/sql/mysql/protocol/NewReader.rs @@ -122,8 +122,6 @@ impl NewReader { /// MySQL's u24/i24 are NOT routed through this trait — see `int_u24`/`int_i24`. pub use bun_core::NativeEndianInt as ReadableInt; -pub type NewReaderOf = NewReader; - impl From for NewReader { fn from(wrapped: C) -> Self { Self { wrapped } @@ -142,11 +140,4 @@ pub trait Decode: Sized { ) -> Result<(), AnyMySQLError> { self.decode_internal(context.into()) } - - fn decode_allocator( - &mut self, - context: impl Into>, - ) -> Result<(), AnyMySQLError> { - self.decode_internal(context.into()) - } } diff --git a/src/sql/mysql/protocol/StackReader.rs b/src/sql/mysql/protocol/StackReader.rs index d996366948db..57415004c5d2 100644 --- a/src/sql/mysql/protocol/StackReader.rs +++ b/src/sql/mysql/protocol/StackReader.rs @@ -1,116 +1,40 @@ -use core::cell::Cell; - -use bun_core::strings; - use super::any_mysql_error::Error as AnyMySQLError; use super::new_reader::{NewReader, ReaderContext}; use crate::shared::data::Data; +use crate::shared::stack_reader::{ShortRead, WrapReader}; -#[derive(Clone, Copy)] -pub struct StackReader<'a> { - pub buffer: &'a [u8], - pub offset: &'a Cell, - pub message_start: &'a Cell, -} - -impl<'a> StackReader<'a> { - pub fn mark_message_start(&self) { - self.message_start.set(self.offset.get()); - } - - pub fn set_offset_from_start(&self, offset: usize) { - self.offset.set(self.message_start.get() + offset); - } - - pub fn ensure_capacity(&self, length: usize) -> bool { - self.offset - .get() - .checked_add(length) - .is_some_and(|end| self.buffer.len() >= end) - } - - pub fn init( - buffer: &'a [u8], - offset: &'a Cell, - message_start: &'a Cell, - ) -> NewReader> { - NewReader { - wrapped: StackReader { - buffer, - offset, - message_start, - }, - } - } +pub use crate::shared::stack_reader::StackReader; - pub fn peek(&self) -> &'a [u8] { - &self.buffer[self.offset.get()..] - } - - pub fn skip(&self, count: isize) { - if count < 0 { - let abs_count = count.unsigned_abs(); - if abs_count > self.offset.get() { - self.offset.set(0); - return; - } - self.offset.set(self.offset.get() - abs_count); - return; - } - - let ucount: usize = usize::try_from(count).expect("int cast"); - if self.offset.get() + ucount > self.buffer.len() { - self.offset.set(self.buffer.len()); - return; - } - - self.offset.set(self.offset.get() + ucount); - } - - pub fn read(&self, count: usize) -> Result { - let offset = self.offset.get(); - if !self.ensure_capacity(count) { - return Err(AnyMySQLError::ShortRead); - } - - self.skip(isize::try_from(count).expect("int cast")); - Ok(Data::Temporary(bun_ptr::RawSlice::new( - &self.buffer[offset..self.offset.get()], - ))) - } - - pub fn read_z(&self) -> Result { - let remaining = self.peek(); - if let Some(zero) = strings::index_of_char(remaining, 0) { - let zero = zero as usize; - self.skip(isize::try_from(zero + 1).expect("int cast")); - return Ok(Data::Temporary(bun_ptr::RawSlice::new(&remaining[0..zero]))); - } +impl ShortRead for AnyMySQLError { + const SHORT_READ: Self = AnyMySQLError::ShortRead; +} - Err(AnyMySQLError::ShortRead) +impl<'a> WrapReader<'a> for NewReader> { + fn wrap(reader: StackReader<'a>) -> Self { + NewReader { wrapped: reader } } } impl<'a> ReaderContext for StackReader<'a> { fn mark_message_start(self) { - Self::mark_message_start(&self) + StackReader::mark_message_start(&self) } fn peek(&self) -> &[u8] { - Self::peek(self) + StackReader::peek(self) } fn skip(self, count: isize) { - Self::skip(&self, count) + StackReader::skip(&self, count) } fn ensure_capacity(self, count: usize) -> bool { - Self::ensure_capacity(&self, count) + StackReader::ensure_capacity(&self, count) } fn read(self, count: usize) -> Result { - Self::read(&self, count) + StackReader::read(&self, count) } fn read_z(self) -> Result { - Self::read_z(&self) + StackReader::read_z(&self) } fn set_offset_from_start(self, offset: usize) { - Self::set_offset_from_start(&self, offset) + StackReader::set_offset_from_start(&self, offset) } } diff --git a/src/sql/postgres/protocol/ErrorResponse.rs b/src/sql/postgres/protocol/ErrorResponse.rs index 0930a1601a76..3f7ac272cf23 100644 --- a/src/sql/postgres/protocol/ErrorResponse.rs +++ b/src/sql/postgres/protocol/ErrorResponse.rs @@ -41,6 +41,20 @@ impl ErrorResponse { ) -> Result { Self::decode_internal(NewReader { wrapped: context }) } + + /// `NoticeResponse` decode: a declared length below 4 decodes as an empty + /// notice instead of failing, unlike `ErrorResponse`. + pub fn decode_notice_internal( + mut reader: NewReader, + ) -> Result { + let remaining_bytes = reader.length()?.saturating_sub(4); + if remaining_bytes > 0 { + return Ok(Self { + messages: FieldMessage::decode_list::(reader)?, + }); + } + Ok(Self::default()) + } } // `to_js` lives on an extension trait in the `bun_sql_jsc` crate. diff --git a/src/sql/postgres/protocol/FieldMessage.rs b/src/sql/postgres/protocol/FieldMessage.rs index 47967a306abb..43fae887007b 100644 --- a/src/sql/postgres/protocol/FieldMessage.rs +++ b/src/sql/postgres/protocol/FieldMessage.rs @@ -29,6 +29,13 @@ pub enum FieldMessage { impl fmt::Display for FieldMessage { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.payload()) + } +} + +impl FieldMessage { + /// Every variant carries a single `bun.String` payload. + pub fn payload(&self) -> &String { match self { FieldMessage::Severity(s) | FieldMessage::LocalizedSeverity(s) @@ -47,12 +54,10 @@ impl fmt::Display for FieldMessage { | FieldMessage::Constraint(s) | FieldMessage::File(s) | FieldMessage::Line(s) - | FieldMessage::Routine(s) => write!(f, "{s}"), + | FieldMessage::Routine(s) => s, } } -} -impl FieldMessage { pub fn decode_list( mut reader: NewReader, ) -> Result, AnyPostgresError> { diff --git a/src/sql/postgres/protocol/NoticeResponse.rs b/src/sql/postgres/protocol/NoticeResponse.rs index 07fb345f33ed..6ba4dfb3bb3a 100644 --- a/src/sql/postgres/protocol/NoticeResponse.rs +++ b/src/sql/postgres/protocol/NoticeResponse.rs @@ -1,35 +1,5 @@ -use super::field_message::FieldMessage; -use super::new_reader::NewReader; -use crate::postgres::AnyPostgresError; - -#[derive(Default)] -pub struct NoticeResponse { - pub messages: Vec, -} - -// Vec drops each element (FieldMessage's Drop) and the buffer -// automatically, so no explicit Drop body is needed. - -impl NoticeResponse { - pub fn decode_internal( - mut reader: NewReader, - ) -> Result { - let mut remaining_bytes = reader.length()?; - remaining_bytes = remaining_bytes.saturating_sub(4); - - if remaining_bytes > 0 { - return Ok(Self { - messages: FieldMessage::decode_list::(reader)?, - }); - } - Ok(Self::default()) - } - - pub fn decode( - context: Container, - ) -> Result { - Self::decode_internal(NewReader { wrapped: context }) - } -} - -// `to_js` lives as an extension-trait method in the bun_sql_jsc crate. +/// NoticeResponse has the same wire format as ErrorResponse — a length-prefixed +/// list of field messages — so it reuses the same type. Notices decode via +/// `decode_notice_internal`, which tolerates a declared length below 4 +/// (decoding as empty) where `ErrorResponse` fails. +pub type NoticeResponse = crate::postgres::protocol::error_response::ErrorResponse; diff --git a/src/sql/postgres/protocol/StackReader.rs b/src/sql/postgres/protocol/StackReader.rs index 411f746af09e..9babc82d3e35 100644 --- a/src/sql/postgres/protocol/StackReader.rs +++ b/src/sql/postgres/protocol/StackReader.rs @@ -1,101 +1,39 @@ use crate::postgres::any_postgres_error::AnyPostgresError; use crate::postgres::protocol::new_reader::{NewReader, ReaderContext}; use crate::shared::data::Data; -use bun_core::strings; +use crate::shared::stack_reader::{ShortRead, WrapReader}; -pub struct StackReader<'a> { - pub buffer: &'a [u8], - pub offset: &'a mut usize, - pub message_start: &'a mut usize, -} - -impl<'a> StackReader<'a> { - pub fn mark_message_start(&mut self) { - *self.message_start = *self.offset; - } - - pub fn ensure_length(&self, length: usize) -> bool { - self.buffer.len() >= (*self.offset + length) - } - - pub fn init( - buffer: &'a [u8], - offset: &'a mut usize, - message_start: &'a mut usize, - ) -> NewReader> { - NewReader { - wrapped: StackReader { - buffer, - offset, - message_start, - }, - } - } - - pub fn peek(&self) -> &[u8] { - &self.buffer[*self.offset..] - } +pub use crate::shared::stack_reader::StackReader; - pub fn skip(&mut self, count: usize) { - if *self.offset + count > self.buffer.len() { - *self.offset = self.buffer.len(); - return; - } - - *self.offset += count; - } - - pub fn ensure_capacity(&self, count: usize) -> bool { - self.buffer.len() >= (*self.offset + count) - } - - pub fn read(&mut self, count: usize) -> Result { - let offset = *self.offset; - if !self.ensure_capacity(count) { - return Err(AnyPostgresError::ShortRead); - } - - self.skip(count); - // Copy the &'a [u8] out before slicing so the returned Data borrows 'a, - // not &mut self. - let buffer: &'a [u8] = self.buffer; - Ok(Data::Temporary(bun_ptr::RawSlice::new( - &buffer[offset..*self.offset], - ))) - } - - pub fn read_z(&mut self) -> Result { - // Inline `peek()` so `remaining` borrows 'a (via the Copy &'a [u8]) - // instead of &self, allowing `self.skip()` below. - let buffer: &'a [u8] = self.buffer; - let remaining = &buffer[*self.offset..]; - if let Some(zero) = strings::index_of_char(remaining, 0) { - let zero = zero as usize; - self.skip(zero + 1); - return Ok(Data::Temporary(bun_ptr::RawSlice::new(&remaining[0..zero]))); - } +impl ShortRead for AnyPostgresError { + const SHORT_READ: Self = AnyPostgresError::ShortRead; +} - Err(AnyPostgresError::ShortRead) +impl<'a> WrapReader<'a> for NewReader> { + fn wrap(reader: StackReader<'a>) -> Self { + NewReader { wrapped: reader } } } impl<'a> ReaderContext for StackReader<'a> { fn mark_message_start(&mut self) { - Self::mark_message_start(self) + StackReader::mark_message_start(self) } fn peek(&self) -> &[u8] { - Self::peek(self) + StackReader::peek(self) } fn skip(&mut self, count: usize) { - Self::skip(self, count) + // The shared reader's signed skip clamps to the buffer end, matching + // the old unsigned behavior even when `count` exceeds `isize::MAX`. + StackReader::skip(self, isize::try_from(count).unwrap_or(isize::MAX)) } fn ensure_length(&mut self, count: usize) -> bool { - Self::ensure_length(self, count) + StackReader::ensure_capacity(self, count) } fn read(&mut self, count: usize) -> Result { - Self::read(self, count) + StackReader::read(self, count) } fn read_z(&mut self) -> Result { - Self::read_z(self) + StackReader::read_z(self) } } diff --git a/src/sql/mysql/QueryStatus.rs b/src/sql/shared/QueryStatus.rs similarity index 100% rename from src/sql/mysql/QueryStatus.rs rename to src/sql/shared/QueryStatus.rs diff --git a/src/sql/shared/StackReader.rs b/src/sql/shared/StackReader.rs new file mode 100644 index 000000000000..1919622b9bc1 --- /dev/null +++ b/src/sql/shared/StackReader.rs @@ -0,0 +1,116 @@ +use core::cell::Cell; + +use bun_core::strings; + +use super::data::Data; + +/// Supplies the protocol error enum's "buffer exhausted" variant for +/// [`StackReader`]'s fallible reads. +pub trait ShortRead { + const SHORT_READ: Self; +} + +/// Wraps a [`StackReader`] in the protocol's reader type ([`StackReader::init`]). +pub trait WrapReader<'a>: Sized { + fn wrap(reader: StackReader<'a>) -> Self; +} + +/// Accepts either `&Cell` or `&mut usize` as a cursor slot in +/// [`StackReader::init`]. +pub trait IntoCursor<'a> { + fn into_cursor(self) -> &'a Cell; +} + +impl<'a> IntoCursor<'a> for &'a Cell { + fn into_cursor(self) -> &'a Cell { + self + } +} + +impl<'a> IntoCursor<'a> for &'a mut usize { + fn into_cursor(self) -> &'a Cell { + Cell::from_mut(self) + } +} + +/// Cursor over a borrowed wire buffer. `Cell`-based so copies share the +/// offset and callers can read the cursor back after a short read. +#[derive(Clone, Copy)] +pub struct StackReader<'a> { + pub buffer: &'a [u8], + pub offset: &'a Cell, + pub message_start: &'a Cell, +} + +impl<'a> StackReader<'a> { + pub fn init>( + buffer: &'a [u8], + offset: impl IntoCursor<'a>, + message_start: impl IntoCursor<'a>, + ) -> R { + R::wrap(StackReader { + buffer, + offset: offset.into_cursor(), + message_start: message_start.into_cursor(), + }) + } + + pub fn mark_message_start(&self) { + self.message_start.set(self.offset.get()); + } + + pub fn set_offset_from_start(&self, offset: usize) { + self.offset.set(self.message_start.get() + offset); + } + + pub fn ensure_capacity(&self, length: usize) -> bool { + self.offset + .get() + .checked_add(length) + .is_some_and(|end| self.buffer.len() >= end) + } + + pub fn peek(&self) -> &'a [u8] { + &self.buffer[self.offset.get()..] + } + + /// Clamps to `[0, buffer.len()]` in both directions. + pub fn skip(&self, count: isize) { + let offset = self.offset.get(); + if count < 0 { + self.offset.set(offset.saturating_sub(count.unsigned_abs())); + return; + } + + let ucount = count.unsigned_abs(); + if offset + ucount > self.buffer.len() { + self.offset.set(self.buffer.len()); + return; + } + + self.offset.set(offset + ucount); + } + + pub fn read(&self, count: usize) -> Result { + let offset = self.offset.get(); + if !self.ensure_capacity(count) { + return Err(E::SHORT_READ); + } + + self.offset.set(offset + count); + Ok(Data::Temporary(bun_ptr::RawSlice::new( + &self.buffer[offset..offset + count], + ))) + } + + pub fn read_z(&self) -> Result { + let remaining = self.peek(); + if let Some(zero) = strings::index_of_char(remaining, 0) { + let zero = zero as usize; + self.skip(isize::try_from(zero + 1).expect("int cast")); + return Ok(Data::Temporary(bun_ptr::RawSlice::new(&remaining[0..zero]))); + } + + Err(E::SHORT_READ) + } +} diff --git a/src/sql/shared/StatementStatus.rs b/src/sql/shared/StatementStatus.rs new file mode 100644 index 000000000000..bce4a693f311 --- /dev/null +++ b/src/sql/shared/StatementStatus.rs @@ -0,0 +1,13 @@ +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum Status { + Pending, + Parsing, + Prepared, + Failed, +} + +impl Status { + pub fn is_running(self) -> bool { + self == Status::Parsing + } +} diff --git a/src/sql_jsc/lib.rs b/src/sql_jsc/lib.rs index 4d25a4a93b40..4a47a8066a87 100644 --- a/src/sql_jsc/lib.rs +++ b/src/sql_jsc/lib.rs @@ -20,6 +20,8 @@ pub mod shared { #[path = "CachedStructure.rs"] pub mod cached_structure; + pub mod connection_ctor_args; + pub mod datetime_text; #[path = "ObjectIterator.rs"] @@ -28,6 +30,8 @@ pub mod shared { #[path = "QueryBindingIterator.rs"] pub mod query_binding_iterator; + pub mod query_ctor_args; + #[path = "SQLDataCell.rs"] pub mod sql_data_cell; diff --git a/src/sql_jsc/mysql/JSMySQLConnection.rs b/src/sql_jsc/mysql/JSMySQLConnection.rs index 8c3925b1cecf..36662fb9cf75 100644 --- a/src/sql_jsc/mysql/JSMySQLConnection.rs +++ b/src/sql_jsc/mysql/JSMySQLConnection.rs @@ -4,11 +4,11 @@ use core::ffi::c_void; use crate::jsc::{ CallFrame, EventLoopSqlExt as _, EventLoopTimer, EventLoopTimerState, EventLoopTimerTag, GlobalRef, HasAutoFlush, JSGlobalObject, JSValue, JsCell, JsRef, JsResult, KeepAlive, - VirtualMachine, VirtualMachineSqlExt as _, api::server_config::SSLConfig, - codegen::js_mysql_connection as js, webcore::AutoFlusher, + VirtualMachine, VirtualMachineSqlExt as _, codegen::js_mysql_connection as js, + webcore::AutoFlusher, }; use crate::shared::CachedStructure; -use bun_boringssl_sys as boringssl; +use crate::shared::connection_ctor_args::{self, ConnectionCtorArgs}; use bun_core::strings; use bun_core::{TimespecMockMode, timespec}; use bun_ptr::{AsCtxPtr, BackRef, ParentRef}; @@ -470,75 +470,15 @@ impl JSMySQLConnection { // no other live borrow in this scope. let vm = global_object.bun_vm().as_mut(); let arguments = callframe.arguments(); - let hostname_str = bun_core::OwnedString::new(arguments[0].to_bun_string(global_object)?); - let port = arguments[1].coerce::(global_object)?; - - let username_str = bun_core::OwnedString::new(arguments[2].to_bun_string(global_object)?); - let password_str = bun_core::OwnedString::new(arguments[3].to_bun_string(global_object)?); - let database_str = bun_core::OwnedString::new(arguments[4].to_bun_string(global_object)?); - // TODO: update this to match MySQL. - let ssl_mode: SSLMode = match arguments[5].to_int32() { - 0 => SSLMode::Disable, - 1 => SSLMode::Prefer, - 2 => SSLMode::Require, - 3 => SSLMode::VerifyCa, - 4 => SSLMode::VerifyFull, - _ => SSLMode::Disable, + let Some(args) = ConnectionCtorArgs::::parse(global_object, &mut *vm, arguments)? + else { + return Ok(JSValue::ZERO); }; - - let tls_object = arguments[6]; - - let mut tls_config: SSLConfig = SSLConfig::default(); - let mut secure: Option<*mut uws::SslCtx> = None; - if ssl_mode != SSLMode::Disable { - tls_config = if tls_object.is_boolean() && tls_object.to_boolean() { - SSLConfig::default() - } else if tls_object.is_object() { - match SSLConfig::from_js(&mut *vm, global_object, tls_object) { - Ok(Some(c)) => c, - Ok(None) => SSLConfig::default(), - Err(_) => return Ok(JSValue::ZERO), - } - } else { - return Err(global_object - .throw_invalid_arguments(format_args!("tls must be a boolean or an object"))); - }; - - if global_object.has_exception() { - drop(tls_config); - return Ok(JSValue::ZERO); - } - - // We always request the cert so we can verify it and also we manually - // abort the connection if the hostname doesn't match. Built here so - // CA/cert errors throw synchronously, applied later by upgradeToTLS. - // Goes through the per-VM weak `SSLContextCache` so every pooled - // connection / reconnect shares one `SSL_CTX*` per distinct config. - let mut err = uws::create_bun_socket_error_t::none; - secure = vm - .ssl_ctx_cache() - .get_or_create_opts(&tls_config.as_usockets_for_client_verification(), &mut err); - if secure.is_none() { - drop(tls_config); - return Err( - global_object.throw_value(crate::jsc::create_bun_socket_error_to_js( - err, - global_object, - )), - ); - } - } // Covers `try arguments[7/8].toBunString()` and the null-byte rejection // below. Ownership passes to `MySQLConnection.init` once `Box::new` // succeeds — we null the locals at that point so the connect-fail path // (which `deref()`s the connection) doesn't double-free. - let tls_guard = scopeguard::guard((secure, tls_config), |(s, cfg)| { - if let Some(s) = s { - // SAFETY: secure was created by ssl_ctx_cache; we own one ref until transferred. - unsafe { boringssl::SSL_CTX_free(s) }; - } - drop(cfg); - }); + let tls_guard = connection_ctor_args::guard_tls(args.secure, args.tls_config); let options_str = bun_core::OwnedString::new(arguments[7].to_bun_string(global_object)?); let path_str = bun_core::OwnedString::new(arguments[8].to_bun_string(global_object)?); @@ -546,9 +486,9 @@ impl JSMySQLConnection { // `init` takes `Box<[u8]>` per field (each separately owned), so we // copy each string into its own allocation. `options_buf` becomes an // empty box. - let username: Box<[u8]> = Box::from(username_str.to_utf8_without_ref().slice()); - let password: Box<[u8]> = Box::from(password_str.to_utf8_without_ref().slice()); - let database: Box<[u8]> = Box::from(database_str.to_utf8_without_ref().slice()); + let username: Box<[u8]> = Box::from(args.username_str.to_utf8_without_ref().slice()); + let password: Box<[u8]> = Box::from(args.password_str.to_utf8_without_ref().slice()); + let database: Box<[u8]> = Box::from(args.database_str.to_utf8_without_ref().slice()); let options: Box<[u8]> = Box::from(options_str.to_utf8_without_ref().slice()); let path: Box<[u8]> = Box::from(path_str.to_utf8_without_ref().slice()); let options_buf: Box<[u8]> = Box::default(); @@ -595,7 +535,7 @@ impl JSMySQLConnection { options_buf, tls_config, secure, - ssl_mode, + args.ssl_mode, allow_public_key_retrieval, )), auto_flusher: JsCell::new(AutoFlusher::default()), @@ -616,7 +556,7 @@ impl JSMySQLConnection { let this = ParentRef::from(core::ptr::NonNull::new(ptr).expect("heap::into_raw non-null")); { - let hostname = hostname_str.to_utf8(); + let hostname = args.hostname_str.to_utf8(); // MySQL always opens plain TCP first; STARTTLS adopts into the TLS // group after the SSLRequest exchange. @@ -636,7 +576,7 @@ impl JSMySQLConnection { uws::DispatchKind::Mysql, None, hostname.slice(), - port, + args.port, ptr, false, ) diff --git a/src/sql_jsc/mysql/JSMySQLQuery.rs b/src/sql_jsc/mysql/JSMySQLQuery.rs index efd430326452..bfdb3025ea48 100644 --- a/src/sql_jsc/mysql/JSMySQLQuery.rs +++ b/src/sql_jsc/mysql/JSMySQLQuery.rs @@ -6,6 +6,7 @@ use crate::jsc::{ self as jsc, CallFrame, JSGlobalObject, JSGlobalObjectSqlExt as _, JSValue, JsRef, JsResult, VirtualMachine, VirtualMachineSqlExt as _, }; +use crate::shared::query_ctor_args::QueryCtorArgs; use bun_jsc::JsCell; use bun_ptr::{AsCtxPtr, BackRef, ParentRef}; use bun_sql::mysql::MySQLQueryResult; @@ -95,43 +96,14 @@ impl JSMySQLQuery { global_this: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let arguments = callframe.arguments(); - let mut args = jsc::call_frame::ArgumentsSlice::init(global_this.sql_vm(), arguments); - // defer args.deinit() — handled by Drop - let Some(query) = args.next_eat() else { - return Err(global_this.throw(format_args!("query must be a string"))); - }; - let Some(values) = args.next_eat() else { - return Err(global_this.throw(format_args!("values must be an array"))); - }; - - if !query.is_string() { - return Err(global_this.throw(format_args!("query must be a string"))); - } - - if values.js_type() != jsc::JSType::Array { - return Err(global_this.throw(format_args!("values must be an array"))); - } - - let pending_value: JSValue = args.next_eat().unwrap_or(JSValue::UNDEFINED); - let columns: JSValue = args.next_eat().unwrap_or(JSValue::UNDEFINED); - let js_bigint: JSValue = args.next_eat().unwrap_or(JSValue::FALSE); - let js_simple: JSValue = args.next_eat().unwrap_or(JSValue::FALSE); - - let bigint = js_bigint.is_boolean() && js_bigint.as_boolean(); - let simple = js_simple.is_boolean() && js_simple.as_boolean(); - if simple { - if values.get_length(global_this)? > 0 { - return Err(global_this - .throw_invalid_arguments(format_args!("simple query cannot have parameters"))); - } - if query.get_length(global_this)? >= i32::MAX as u64 { - return Err(global_this.throw_invalid_arguments(format_args!("query is too long"))); - } - } - if !pending_value.js_type().is_array_like() { - return Err(global_this.throw_invalid_argument_type("query", "pendingValue", "Array")); - } + let QueryCtorArgs { + query, + values, + pending_value, + columns, + bigint, + simple, + } = QueryCtorArgs::parse(global_this, callframe.arguments())?; let this_ptr = bun_core::heap::into_raw(Box::new(Self { this_value: JsCell::new(JsRef::empty()), diff --git a/src/sql_jsc/mysql/MySQLStatement.rs b/src/sql_jsc/mysql/MySQLStatement.rs index ea0756d50016..95914708d6c3 100644 --- a/src/sql_jsc/mysql/MySQLStatement.rs +++ b/src/sql_jsc/mysql/MySQLStatement.rs @@ -1,15 +1,13 @@ use core::cell::Cell; use crate::jsc::{JSGlobalObject, JSValue}; -use bun_collections::StringHashMap; use crate::mysql::protocol::Signature; use crate::shared::CachedStructure; -use crate::shared::sql_data_cell::Flags as DataCellFlags; +use crate::shared::sql_data_cell::{Flags as DataCellFlags, dedupe_columns}; use bun_sql::mysql::protocol::column_definition41::ColumnDefinition41; use bun_sql::mysql::protocol::error_packet::ErrorPacket; -use bun_sql::shared::ColumnIdentifier; pub use bun_sql::mysql::mysql_param::Param; @@ -88,13 +86,7 @@ impl Default for ExecutionFlags { } } -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum Status { - Pending, - Parsing, - Prepared, - Failed, -} +pub use bun_sql::shared::statement_status::Status; impl MySQLStatement { /// Set the initial intrusive @@ -124,49 +116,8 @@ impl MySQLStatement { self.execution_flags .remove(ExecutionFlags::NEEDS_DUPLICATE_CHECK); - let mut seen_numbers: Vec = Vec::new(); - let mut seen_fields: StringHashMap<()> = StringHashMap::default(); - seen_fields.reserve(self.columns.len()); - - // iterate backwards - let mut remaining = self.columns.len(); - let mut flags = DataCellFlags::default(); - while remaining > 0 { - remaining -= 1; - let field: &mut ColumnDefinition41 = &mut self.columns[remaining]; - match &field.name_or_index { - ColumnIdentifier::Name(name) => { - // reshaped for borrowck — compute `found_existing` before - // mutating `field.name_or_index`. - let found_existing = seen_fields - .get_or_put(name.slice()) - .expect("OOM") - .found_existing; - if found_existing { - field.name_or_index = ColumnIdentifier::Duplicate; - flags.insert(DataCellFlags::HAS_DUPLICATE_COLUMNS); - } - - flags.insert(DataCellFlags::HAS_NAMED_COLUMNS); - } - ColumnIdentifier::Index(index) => { - let index = *index; - if seen_numbers.contains(&index) { - field.name_or_index = ColumnIdentifier::Duplicate; - flags.insert(DataCellFlags::HAS_DUPLICATE_COLUMNS); - } else { - seen_numbers.push(index); - } - - flags.insert(DataCellFlags::HAS_INDEXED_COLUMNS); - } - ColumnIdentifier::Duplicate => { - flags.insert(DataCellFlags::HAS_DUPLICATE_COLUMNS); - } - } - } - - self.fields_flags = flags; + self.fields_flags = + dedupe_columns(self.columns.iter_mut().rev().map(|c| &mut c.name_or_index)); } // Returning `&CachedStructure` diff --git a/src/sql_jsc/postgres.rs b/src/sql_jsc/postgres.rs index b3ef8eec8059..eb20c6d831d0 100644 --- a/src/sql_jsc/postgres.rs +++ b/src/sql_jsc/postgres.rs @@ -85,9 +85,6 @@ pub mod types { pub mod protocol { #[path = "error_response_jsc.rs"] pub mod error_response_jsc; - - #[path = "notice_response_jsc.rs"] - pub mod notice_response_jsc; } // Re-exports of base-crate protocol/types modules. diff --git a/src/sql_jsc/postgres/PostgresSQLConnection.rs b/src/sql_jsc/postgres/PostgresSQLConnection.rs index b41ab1dfeb67..3b5f1fa96061 100644 --- a/src/sql_jsc/postgres/PostgresSQLConnection.rs +++ b/src/sql_jsc/postgres/PostgresSQLConnection.rs @@ -31,6 +31,7 @@ use crate::postgres::postgres_sql_query::{self, Status as QueryStatus}; use crate::postgres::postgres_sql_statement::{Error as StatementError, Status as StatementStatus}; use crate::postgres::sasl::SASLStatus; use crate::shared::CachedStructure as PostgresCachedStructure; +use crate::shared::connection_ctor_args::{self, ConnectionCtorArgs}; use bun_sql::postgres::AnyPostgresError; use bun_sql::postgres::PostgresErrorOptions; use bun_sql::postgres::PostgresProtocol as protocol; @@ -1056,77 +1057,15 @@ pub(crate) fn call(global_object: &JSGlobalObject, callframe: &CallFrame) -> JsR // `&mut self` helpers like `ssl_ctx_cache()` / `postgres_socket_group()`. let vm = global_object.bun_vm().as_mut(); let arguments = callframe.arguments(); - let hostname_str = bun_core::OwnedString::new(arguments[0].to_bun_string(global_object)?); - let port = arguments[1].coerce::(global_object)?; - - let username_str = bun_core::OwnedString::new(arguments[2].to_bun_string(global_object)?); - let password_str = bun_core::OwnedString::new(arguments[3].to_bun_string(global_object)?); - let database_str = bun_core::OwnedString::new(arguments[4].to_bun_string(global_object)?); - let ssl_mode: SSLMode = match arguments[5].to_int32() { - 0 => SSLMode::Disable, - 1 => SSLMode::Prefer, - 2 => SSLMode::Require, - 3 => SSLMode::VerifyCa, - 4 => SSLMode::VerifyFull, - _ => SSLMode::Disable, + let Some(args) = ConnectionCtorArgs::::parse(global_object, &mut *vm, arguments)? + else { + return Ok(JSValue::ZERO); }; - - let tls_object = arguments[6]; - - let mut tls_config: jsc::api::ServerConfig::SSLConfig = Default::default(); - let mut secure: Option<*mut uws::SslCtx> = None; - if ssl_mode != SSLMode::Disable { - tls_config = if tls_object.is_boolean() && tls_object.to_boolean() { - Default::default() - } else if tls_object.is_object() { - match jsc::api::ServerConfig::SSLConfig::from_js(&mut *vm, global_object, tls_object) { - Ok(opt) => opt.unwrap_or_default(), - Err(_) => return Ok(JSValue::ZERO), - } - } else { - return Err(global_object - .throw_invalid_arguments(format_args!("tls must be a boolean or an object"))); - }; - - if global_object.has_exception() { - drop(tls_config); - return Ok(JSValue::ZERO); - } - - // We always request the cert so we can verify it and also we manually - // abort the connection if the hostname doesn't match. Built here (not - // at STARTTLS time) so cert/CA errors throw synchronously. Goes - // through the per-VM weak `SSLContextCache` so every connection in the - // pool — and every reconnect — shares one `SSL_CTX*` per distinct - // config instead of building a fresh one per `PostgresSQLConnection`. - let mut err: uws::create_bun_socket_error_t = uws::create_bun_socket_error_t::none; - secure = vm - .ssl_ctx_cache() - .get_or_create_opts(&tls_config.as_usockets_for_client_verification(), &mut err); - if secure.is_none() { - drop(tls_config); - return Err( - global_object.throw_value(crate::jsc::create_bun_socket_error_to_js( - err, - global_object, - )), - ); - } - } // Covers `try arguments[7/8].toBunString()` and the null-byte rejection // below. Ownership passes into `ptr.*` once allocated — `into_inner` // recovers them just before the Box is built so the connect-fail path's // `ptr.deinit()` is the sole cleanup. - // guard owns `(secure, tls_config)` by value. Do NOT - // `drop_in_place` a stack local that Rust would also auto-drop on unwind — - // that double-frees. The closure's `_tls_config` is dropped exactly once by - // normal scope-exit drop here. - let errdefer_guard = scopeguard::guard((secure, tls_config), |(secure, _tls_config)| { - if let Some(s) = secure { - // SAFETY: SSL_CTX_free is safe to call on a valid SSL_CTX*. - unsafe { BoringSSL::c::SSL_CTX_free(s) }; - } - }); + let errdefer_guard = connection_ctor_args::guard_tls(args.secure, args.tls_config); // `StringBuilder::append` takes `&mut self` and returns a borrow // of the backing buffer, so successive appends can't keep their `&[u8]` @@ -1146,11 +1085,11 @@ pub(crate) fn call(global_object: &JSGlobalObject, callframe: &CallFrame) -> JsR let options_buf: Box<[u8]> = 'brk: { let mut b = bun_core::StringBuilder::default(); - b.cap += username_str.utf8_byte_length() + b.cap += args.username_str.utf8_byte_length() + 1 - + password_str.utf8_byte_length() + + args.password_str.utf8_byte_length() + 1 - + database_str.utf8_byte_length() + + args.database_str.utf8_byte_length() + 1 + options_str.utf8_byte_length() + 1 @@ -1158,15 +1097,15 @@ pub(crate) fn call(global_object: &JSGlobalObject, callframe: &CallFrame) -> JsR + 1; let _ = b.allocate(); - let u = username_str.to_utf8_without_ref(); + let u = args.username_str.to_utf8_without_ref(); username = bun_ptr::RawSlice::new(b.append(u.slice())); drop(u); - let p = password_str.to_utf8_without_ref(); + let p = args.password_str.to_utf8_without_ref(); password = bun_ptr::RawSlice::new(b.append(p.slice())); drop(p); - let d = database_str.to_utf8_without_ref(); + let d = args.database_str.to_utf8_without_ref(); database = bun_ptr::RawSlice::new(b.append(d.slice())); drop(d); @@ -1247,12 +1186,12 @@ pub(crate) fn call(global_object: &JSGlobalObject, callframe: &CallFrame) -> JsR authentication_state: JsCell::new(AuthenticationState::Pending), secure, tls_config, - tls_status: Cell::new(if ssl_mode != SSLMode::Disable { + tls_status: Cell::new(if args.ssl_mode != SSLMode::Disable { TLSStatus::Pending } else { TLSStatus::None }), - ssl_mode, + ssl_mode: args.ssl_mode, idle_timeout_interval_ms: u32::try_from(idle_timeout).expect("int cast"), connection_timeout_ms: u32::try_from(connection_timeout).expect("int cast"), flags: Cell::new(if use_unnamed_prepared_statements { @@ -1276,7 +1215,7 @@ pub(crate) fn call(global_object: &JSGlobalObject, callframe: &CallFrame) -> JsR let this = ParentRef::from(core::ptr::NonNull::new(ptr).expect("heap::into_raw non-null")); { - let hostname = hostname_str.to_utf8(); + let hostname = args.hostname_str.to_utf8(); // Postgres always opens plain TCP first (SSLRequest happens in-band), // so even `ssl_mode != .disable` lands in the TCP group; `setupTLS()` @@ -1298,7 +1237,7 @@ pub(crate) fn call(global_object: &JSGlobalObject, callframe: &CallFrame) -> JsR uws::SocketKind::Postgres, None, hostname.slice(), - port, + args.port, ptr, false, ) @@ -2991,7 +2930,7 @@ impl PostgresSQLConnection { } MessageType::NoticeResponse => { debug!("UNSUPPORTED NoticeResponse"); - let _resp = protocol::NoticeResponse::decode_internal(reader.reborrow())?; + let _resp = protocol::NoticeResponse::decode_notice_internal(reader.reborrow())?; // _resp dropped at scope end } MessageType::NotificationResponse => { diff --git a/src/sql_jsc/postgres/PostgresSQLQuery.rs b/src/sql_jsc/postgres/PostgresSQLQuery.rs index ac655840ca96..c2c3b45768b6 100644 --- a/src/sql_jsc/postgres/PostgresSQLQuery.rs +++ b/src/sql_jsc/postgres/PostgresSQLQuery.rs @@ -4,6 +4,7 @@ use core::mem; use crate::jsc::{ CallFrame, JSGlobalObject, JSValue, JsError, JsRef, JsResult, VirtualMachineSqlExt as _, }; +use crate::shared::query_ctor_args::QueryCtorArgs; use bun_core::String as BunString; use bun_jsc::JsCell; use bun_ptr::AsCtxPtr; @@ -107,28 +108,7 @@ impl Default for Flags { } } -#[repr(u8)] -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum Status { - /// The query was just enqueued, statement status can be checked for more details - Pending, - /// The query is being bound to the statement - Binding, - /// The query is running - Running, - /// The query is waiting for a partial response - PartialResponse, - /// The query was successful - Success, - /// The query failed - Fail, -} - -impl Status { - pub fn is_running(self) -> bool { - (self as u8) > (Status::Pending as u8) && (self as u8) < (Status::Success as u8) - } -} +pub use bun_sql::shared::query_status::Status; impl PostgresSQLQuery { // `ref_()`/`deref()` provided by `#[derive(CellRefCounted)]`. @@ -382,44 +362,14 @@ impl PostgresSQLQuery { // Registered directly as `createQuery` via // `put_host_functions!` in `postgres.rs`, so no exported symbol is needed. pub fn call(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let arguments = callframe.arguments(); - let mut args = - crate::jsc::call_frame::ArgumentsSlice::init(global_this.bun_vm(), arguments); - // ArgumentsSlice has Drop. - let Some(query) = args.next_eat() else { - return Err(global_this.throw(format_args!("query must be a string"))); - }; - let Some(values) = args.next_eat() else { - return Err(global_this.throw(format_args!("values must be an array"))); - }; - - if !query.is_string() { - return Err(global_this.throw(format_args!("query must be a string"))); - } - - if values.js_type() != crate::jsc::JSType::Array { - return Err(global_this.throw(format_args!("values must be an array"))); - } - - let pending_value: JSValue = args.next_eat().unwrap_or(JSValue::UNDEFINED); - let columns: JSValue = args.next_eat().unwrap_or(JSValue::UNDEFINED); - let js_bigint: JSValue = args.next_eat().unwrap_or(JSValue::FALSE); - let js_simple: JSValue = args.next_eat().unwrap_or(JSValue::FALSE); - - let bigint = js_bigint.is_boolean() && js_bigint.as_boolean(); - let simple = js_simple.is_boolean() && js_simple.as_boolean(); - if simple { - if values.get_length(global_this)? > 0 { - return Err(global_this - .throw_invalid_arguments(format_args!("simple query cannot have parameters"))); - } - if query.get_length(global_this)? >= i32::MAX as u64 { - return Err(global_this.throw_invalid_arguments(format_args!("query is too long"))); - } - } - if !pending_value.js_type().is_array_like() { - return Err(global_this.throw_invalid_argument_type("query", "pendingValue", "Array")); - } + let QueryCtorArgs { + query, + values, + pending_value, + columns, + bigint, + simple, + } = QueryCtorArgs::parse(global_this, callframe.arguments())?; let ptr = bun_core::heap::into_raw(Box::new(PostgresSQLQuery::default())); diff --git a/src/sql_jsc/postgres/PostgresSQLStatement.rs b/src/sql_jsc/postgres/PostgresSQLStatement.rs index 3dafeec82453..a2c4647baba2 100644 --- a/src/sql_jsc/postgres/PostgresSQLStatement.rs +++ b/src/sql_jsc/postgres/PostgresSQLStatement.rs @@ -1,17 +1,15 @@ use core::cell::Cell; use crate::jsc::{JSGlobalObject, JSValue, JsResult}; -use bun_collections::StringHashMap; use crate::postgres::error_jsc::postgres_error_to_js; use crate::postgres::signature::Signature; use crate::shared::cached_structure::CachedStructure as PostgresCachedStructure; -use crate::shared::sql_data_cell::Flags as DataCellFlags; +use crate::shared::sql_data_cell::{Flags as DataCellFlags, dedupe_columns}; use bun_sql::postgres::any_postgres_error::AnyPostgresError; use bun_sql::postgres::postgres_protocol as protocol; use bun_sql::postgres::postgres_types::int4; -use bun_sql::shared::ColumnIdentifier; bun_core::declare_scope!(Postgres, visible); @@ -71,19 +69,7 @@ impl Error { } } -#[derive(Copy, Clone, Eq, PartialEq)] -pub enum Status { - Pending, - Parsing, - Prepared, - Failed, -} - -impl Status { - pub fn is_running(self) -> bool { - self == Status::Parsing - } -} +pub use bun_sql::shared::statement_status::Status; impl PostgresSQLStatement { /// Set the initial intrusive @@ -103,52 +89,8 @@ impl PostgresSQLStatement { } self.needs_duplicate_check = false; - let mut seen_numbers: Vec = Vec::new(); - let mut seen_fields: StringHashMap<()> = StringHashMap::default(); - seen_fields.reserve(self.fields.len()); - - // iterate backwards - let mut remaining = self.fields.len(); - let mut flags = DataCellFlags::default(); - while remaining > 0 { - remaining -= 1; - let field: &mut protocol::FieldDescription = &mut self.fields[remaining]; - match &field.name_or_index { - ColumnIdentifier::Name(name) => { - // Note: reshaped for borrowck — compute `found_existing` - // before mutating `field.name_or_index`. - // StringHashMap - // clones to an owned `Box<[u8]>` key. Fine for a transient - // dedup set. - let found_existing = seen_fields - .get_or_put(name.slice()) - .expect("OOM") - .found_existing; - if found_existing { - field.name_or_index = ColumnIdentifier::Duplicate; - flags.insert(DataCellFlags::HAS_DUPLICATE_COLUMNS); - } - - flags.insert(DataCellFlags::HAS_NAMED_COLUMNS); - } - ColumnIdentifier::Index(index) => { - let index = *index; - if seen_numbers.contains(&index) { - field.name_or_index = ColumnIdentifier::Duplicate; - flags.insert(DataCellFlags::HAS_DUPLICATE_COLUMNS); - } else { - seen_numbers.push(index); - } - - flags.insert(DataCellFlags::HAS_INDEXED_COLUMNS); - } - ColumnIdentifier::Duplicate => { - flags.insert(DataCellFlags::HAS_DUPLICATE_COLUMNS); - } - } - } - - self.fields_flags = flags; + self.fields_flags = + dedupe_columns(self.fields.iter_mut().rev().map(|f| &mut f.name_or_index)); } // Note: returning diff --git a/src/sql_jsc/postgres/protocol/error_response_jsc.rs b/src/sql_jsc/postgres/protocol/error_response_jsc.rs index bedf0fd59e6c..2813335ed897 100644 --- a/src/sql_jsc/postgres/protocol/error_response_jsc.rs +++ b/src/sql_jsc/postgres/protocol/error_response_jsc.rs @@ -7,15 +7,11 @@ use bun_sql::postgres::protocol::field_message::FieldMessage; use crate::postgres::error_jsc::create_postgres_error; use bun_sql::postgres::any_postgres_error::PostgresErrorOptions; -use super::notice_response_jsc::field_message_payload; - pub(crate) fn to_js(this: &ErrorResponse, global_object: &JSGlobalObject) -> JSValue { let mut b = StringBuilder::default(); for msg in this.messages.iter() { - // Every - // FieldMessage variant carries a single bun.String payload. - b.cap += field_message_payload(msg).utf8_byte_length() + 1; + b.cap += msg.payload().utf8_byte_length() + 1; } let _ = b.allocate(); diff --git a/src/sql_jsc/postgres/protocol/notice_response_jsc.rs b/src/sql_jsc/postgres/protocol/notice_response_jsc.rs deleted file mode 100644 index 6aa8cbf31e30..000000000000 --- a/src/sql_jsc/postgres/protocol/notice_response_jsc.rs +++ /dev/null @@ -1,28 +0,0 @@ -use bun_sql::postgres::protocol::field_message::FieldMessage; - -/// Every `FieldMessage` variant -/// carries a single `bun.String` payload, so an exhaustive match collapses to -/// the single binding. The match lives here (not as a `payload()` accessor on -/// `bun_sql::FieldMessage`) because this is its only consumer. -pub(crate) fn field_message_payload(msg: &FieldMessage) -> &bun_core::String { - match msg { - FieldMessage::Severity(s) - | FieldMessage::LocalizedSeverity(s) - | FieldMessage::Code(s) - | FieldMessage::Message(s) - | FieldMessage::Detail(s) - | FieldMessage::Hint(s) - | FieldMessage::Position(s) - | FieldMessage::InternalPosition(s) - | FieldMessage::Internal(s) - | FieldMessage::Where(s) - | FieldMessage::Schema(s) - | FieldMessage::Table(s) - | FieldMessage::Column(s) - | FieldMessage::Datatype(s) - | FieldMessage::Constraint(s) - | FieldMessage::File(s) - | FieldMessage::Line(s) - | FieldMessage::Routine(s) => s, - } -} diff --git a/src/sql_jsc/shared/SQLDataCell.rs b/src/sql_jsc/shared/SQLDataCell.rs index dd84b7104854..9bff2b897f45 100644 --- a/src/sql_jsc/shared/SQLDataCell.rs +++ b/src/sql_jsc/shared/SQLDataCell.rs @@ -2,8 +2,9 @@ use core::ptr; use core::slice; use crate::jsc::{ExternColumnIdentifier, JSGlobalObject, JSType, JSValue, JsError, JsResult}; +use bun_collections::StringHashMap; use bun_core::wtf::WTFStringImpl; -use bun_sql::shared::Data; +use bun_sql::shared::{ColumnIdentifier, Data}; // Note: This entire type is passed by pointer // across FFI to C++ (`JSC__constructObjectFromDataCell`). Field layout is @@ -381,6 +382,55 @@ bitflags::bitflags! { } } +/// Rewrites repeated column identifiers to [`ColumnIdentifier::Duplicate`] and +/// accumulates the column-set [`Flags`]. Callers pass the columns in reverse +/// order so the LAST occurrence of a repeated name/index keeps its identifier. +pub fn dedupe_columns<'a>( + columns: impl ExactSizeIterator, +) -> Flags { + let mut seen_numbers: Vec = Vec::new(); + // StringHashMap clones to an owned `Box<[u8]>` key. Fine for a transient + // dedup set. + let mut seen_fields: StringHashMap<()> = StringHashMap::default(); + seen_fields.reserve(columns.len()); + + let mut flags = Flags::default(); + for name_or_index in columns { + match &*name_or_index { + ColumnIdentifier::Name(name) => { + // reshaped for borrowck — compute `found_existing` before + // mutating `*name_or_index`. + let found_existing = seen_fields + .get_or_put(name.slice()) + .expect("OOM") + .found_existing; + if found_existing { + *name_or_index = ColumnIdentifier::Duplicate; + flags.insert(Flags::HAS_DUPLICATE_COLUMNS); + } + + flags.insert(Flags::HAS_NAMED_COLUMNS); + } + ColumnIdentifier::Index(index) => { + let index = *index; + if seen_numbers.contains(&index) { + *name_or_index = ColumnIdentifier::Duplicate; + flags.insert(Flags::HAS_DUPLICATE_COLUMNS); + } else { + seen_numbers.push(index); + } + + flags.insert(Flags::HAS_INDEXED_COLUMNS); + } + ColumnIdentifier::Duplicate => { + flags.insert(Flags::HAS_DUPLICATE_COLUMNS); + } + } + } + + flags +} + // Declared inline rather than in a dedicated `*_sys` crate: this is the only // extern this crate calls and its sole consumer is the wrapper above. unsafe extern "C" { diff --git a/src/sql_jsc/shared/connection_ctor_args.rs b/src/sql_jsc/shared/connection_ctor_args.rs new file mode 100644 index 000000000000..a606f913628b --- /dev/null +++ b/src/sql_jsc/shared/connection_ctor_args.rs @@ -0,0 +1,133 @@ +//! Shared connection-constructor prologue for the Postgres and MySQL +//! `createConnection(hostname, port, username, password, database, sslMode, +//! tls, ...)` host functions, through the per-VM `SSL_CTX*` cache lookup. + +use crate::jsc::{ + JSGlobalObject, JSValue, JsResult, VirtualMachine, VirtualMachineSqlExt as _, + api::server_config::SSLConfig, +}; +use bun_uws as uws; + +pub(crate) trait SslModeArg: Copy + PartialEq { + /// Wire order of the JS-side enum; index 0 is `Disable`. + const MODES: [Self; 5]; +} + +macro_rules! impl_ssl_mode_arg { + ($ty:ty) => { + impl SslModeArg for $ty { + const MODES: [Self; 5] = [ + Self::Disable, + Self::Prefer, + Self::Require, + Self::VerifyCa, + Self::VerifyFull, + ]; + } + }; +} +// TODO: update the MySQL mapping to match MySQL's own ssl-mode set. +impl_ssl_mode_arg!(bun_sql::mysql::ssl_mode::SSLMode); +impl_ssl_mode_arg!(bun_sql::postgres::SSLMode); + +type GuardState = (Option<*mut uws::SslCtx>, SSLConfig); +pub(crate) type TlsGuard = scopeguard::ScopeGuard; + +/// Errdefer over `(secure, tls_config)`: frees the cached `SSL_CTX*` +/// reference and drops the config unless disarmed via +/// `ScopeGuard::into_inner` once ownership transfers into the connection. +pub(crate) fn guard_tls(secure: Option<*mut uws::SslCtx>, tls_config: SSLConfig) -> TlsGuard { + fn free((secure, _tls_config): GuardState) { + if let Some(s) = secure { + // SAFETY: `secure` holds one `ssl_ctx_cache` reference owned by the caller. + unsafe { bun_boringssl_sys::SSL_CTX_free(s) }; + } + } + scopeguard::guard((secure, tls_config), free as fn(GuardState)) +} + +pub(crate) struct ConnectionCtorArgs { + pub hostname_str: bun_core::OwnedString, + pub port: i32, + pub username_str: bun_core::OwnedString, + pub password_str: bun_core::OwnedString, + pub database_str: bun_core::OwnedString, + pub ssl_mode: M, + pub tls_config: SSLConfig, + /// `SSL_CTX*` holding one reference the caller must release on every + /// early exit (via [`guard_tls`]) until it transfers into the connection. + pub secure: Option<*mut uws::SslCtx>, +} + +impl ConnectionCtorArgs { + /// Parses `arguments[0..=6]`. Returns `Ok(None)` when a JS exception is + /// already pending and the caller should `return Ok(JSValue::ZERO)`. + pub(crate) fn parse( + global_object: &JSGlobalObject, + vm: &mut VirtualMachine, + arguments: &[JSValue], + ) -> JsResult> { + let hostname_str = bun_core::OwnedString::new(arguments[0].to_bun_string(global_object)?); + let port = arguments[1].coerce::(global_object)?; + let username_str = bun_core::OwnedString::new(arguments[2].to_bun_string(global_object)?); + let password_str = bun_core::OwnedString::new(arguments[3].to_bun_string(global_object)?); + let database_str = bun_core::OwnedString::new(arguments[4].to_bun_string(global_object)?); + let modes = M::MODES; + let ssl_mode = usize::try_from(arguments[5].to_int32()) + .ok() + .and_then(|i| modes.get(i)) + .copied() + .unwrap_or(modes[0]); + + let tls_object = arguments[6]; + let mut tls_config = SSLConfig::default(); + let mut secure: Option<*mut uws::SslCtx> = None; + if ssl_mode != modes[0] { + tls_config = if tls_object.is_boolean() && tls_object.to_boolean() { + SSLConfig::default() + } else if tls_object.is_object() { + match SSLConfig::from_js(&mut *vm, global_object, tls_object) { + Ok(opt) => opt.unwrap_or_default(), + Err(_) => return Ok(None), + } + } else { + return Err(global_object + .throw_invalid_arguments(format_args!("tls must be a boolean or an object"))); + }; + + if global_object.has_exception() { + return Ok(None); + } + + // We always request the cert so we can verify it and manually + // abort if the hostname doesn't match. Built here (not at STARTTLS + // time) so cert/CA errors throw synchronously; the per-VM weak + // `SSLContextCache` shares one `SSL_CTX*` per distinct config + // across pooled connections and reconnects. + let mut err = uws::create_bun_socket_error_t::none; + secure = vm + .ssl_ctx_cache() + .get_or_create_opts(&tls_config.as_usockets_for_client_verification(), &mut err); + if secure.is_none() { + drop(tls_config); + return Err( + global_object.throw_value(crate::jsc::create_bun_socket_error_to_js( + err, + global_object, + )), + ); + } + } + + Ok(Some(Self { + hostname_str, + port, + username_str, + password_str, + database_str, + ssl_mode, + tls_config, + secure, + })) + } +} diff --git a/src/sql_jsc/shared/query_ctor_args.rs b/src/sql_jsc/shared/query_ctor_args.rs new file mode 100644 index 000000000000..15378826b991 --- /dev/null +++ b/src/sql_jsc/shared/query_ctor_args.rs @@ -0,0 +1,64 @@ +//! Shared `createQuery(query, values, pendingValue?, columns?, bigint?, +//! simple?)` constructor-argument parsing/validation used by both the +//! Postgres and MySQL query constructors. + +use crate::jsc::{JSGlobalObject, JSGlobalObjectSqlExt as _, JSType, JSValue, JsResult}; + +pub(crate) struct QueryCtorArgs { + pub query: JSValue, + pub values: JSValue, + pub pending_value: JSValue, + pub columns: JSValue, + pub bigint: bool, + pub simple: bool, +} + +impl QueryCtorArgs { + pub(crate) fn parse(global_this: &JSGlobalObject, arguments: &[JSValue]) -> JsResult { + let mut args = + crate::jsc::call_frame::ArgumentsSlice::init(global_this.sql_vm(), arguments); + let Some(query) = args.next_eat() else { + return Err(global_this.throw(format_args!("query must be a string"))); + }; + let Some(values) = args.next_eat() else { + return Err(global_this.throw(format_args!("values must be an array"))); + }; + + if !query.is_string() { + return Err(global_this.throw(format_args!("query must be a string"))); + } + + if values.js_type() != JSType::Array { + return Err(global_this.throw(format_args!("values must be an array"))); + } + + let pending_value: JSValue = args.next_eat().unwrap_or(JSValue::UNDEFINED); + let columns: JSValue = args.next_eat().unwrap_or(JSValue::UNDEFINED); + let js_bigint: JSValue = args.next_eat().unwrap_or(JSValue::FALSE); + let js_simple: JSValue = args.next_eat().unwrap_or(JSValue::FALSE); + + let bigint = js_bigint.is_boolean() && js_bigint.as_boolean(); + let simple = js_simple.is_boolean() && js_simple.as_boolean(); + if simple { + if values.get_length(global_this)? > 0 { + return Err(global_this + .throw_invalid_arguments(format_args!("simple query cannot have parameters"))); + } + if query.get_length(global_this)? >= i32::MAX as u64 { + return Err(global_this.throw_invalid_arguments(format_args!("query is too long"))); + } + } + if !pending_value.js_type().is_array_like() { + return Err(global_this.throw_invalid_argument_type("query", "pendingValue", "Array")); + } + + Ok(Self { + query, + values, + pending_value, + columns, + bigint, + simple, + }) + } +} diff --git a/src/standalone_graph/StandaloneModuleGraph.rs b/src/standalone_graph/StandaloneModuleGraph.rs index 244153038a43..fd5240c65fc3 100644 --- a/src/standalone_graph/StandaloneModuleGraph.rs +++ b/src/standalone_graph/StandaloneModuleGraph.rs @@ -1962,97 +1962,51 @@ impl StandaloneModuleGraph { /// Loads the standalone module graph from the executable, allocates it on the heap, /// sets it globally, and returns the pointer. pub fn from_executable() -> Result, BunError> { - #[cfg(target_os = "macos")] - { - let Some((base, len)) = macho::get_data() else { - return Ok(None); - }; - if len < size_of::() + TRAILER.len() { - bun_core::debug_warn!("bun standalone module graph is too small to be valid"); - return Ok(None); - } - // SAFETY: `[len - Offsets - TRAILER, len)` is in-bounds (checked above) and - // read-only; build short-lived views via raw `read_unaligned` so no `&[u8]` - // ever spans the writable bytecode region carried in `base`'s provenance. - let offsets_ptr = unsafe { base.add(len - size_of::() - TRAILER.len()) }; - // SAFETY: `[len - TRAILER.len(), len)` is in-bounds (length checked above) and read-only. - let trailer_bytes = unsafe { - core::slice::from_raw_parts(base.add(len - TRAILER.len()), TRAILER.len()) - }; - if trailer_bytes != TRAILER { - bun_core::debug_warn!("bun standalone module graph has invalid trailer"); - return Ok(None); - } - // SAFETY: offsets_ptr has at least size_of::() bytes. - let offsets: Offsets = - unsafe { core::ptr::read_unaligned(offsets_ptr.cast::()) }; - return from_bytes_alloc(base, len, offsets).map(Some); - } - - #[cfg(windows)] - { - let Some((base, len)) = pe::get_data() else { - return Ok(None); - }; - if len < size_of::() + TRAILER.len() { - bun_core::debug_warn!("bun standalone module graph is too small to be valid"); - return Ok(None); + let data = { + #[cfg(target_os = "macos")] + { + macho::get_data() } - // SAFETY: `[len - Offsets - TRAILER, len)` is in-bounds (checked above) and - // read-only; build short-lived views via raw `read_unaligned` so no `&[u8]` - // ever spans the writable bytecode region carried in `base`'s provenance. - let offsets_ptr = unsafe { base.add(len - size_of::() - TRAILER.len()) }; - // SAFETY: `[len - TRAILER.len(), len)` is in-bounds (length checked above) and read-only. - let trailer_bytes = unsafe { - core::slice::from_raw_parts(base.add(len - TRAILER.len()), TRAILER.len()) - }; - if trailer_bytes != TRAILER { - bun_core::debug_warn!("bun standalone module graph has invalid trailer"); - return Ok(None); + #[cfg(windows)] + { + pe::get_data() } - // SAFETY: offsets_ptr has at least size_of::() bytes. - let offsets: Offsets = - unsafe { core::ptr::read_unaligned(offsets_ptr.cast::()) }; - return from_bytes_alloc(base, len, offsets).map(Some); - } - - #[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))] - { - let Some((base, len)) = elf::get_data() else { - return Ok(None); - }; - if len < size_of::() + TRAILER.len() { - bun_core::debug_warn!("bun standalone module graph is too small to be valid"); - return Ok(None); + #[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))] + { + elf::get_data() } - // SAFETY: `[len - Offsets - TRAILER, len)` is in-bounds (checked above) and - // read-only; build short-lived views via raw `read_unaligned` so no `&[u8]` - // ever spans the writable bytecode region carried in `base`'s provenance. - let offsets_ptr = unsafe { base.add(len - size_of::() - TRAILER.len()) }; - // SAFETY: `[len - TRAILER.len(), len)` is in-bounds (length checked above) and read-only. - let trailer_bytes = unsafe { - core::slice::from_raw_parts(base.add(len - TRAILER.len()), TRAILER.len()) - }; - if trailer_bytes != TRAILER { - bun_core::debug_warn!("bun standalone module graph has invalid trailer"); - return Ok(None); + #[cfg(not(any( + target_os = "macos", + windows, + target_os = "linux", + target_os = "android", + target_os = "freebsd" + )))] + { + unreachable!() } - // SAFETY: offsets_ptr has at least size_of::() bytes. - let offsets: Offsets = - unsafe { core::ptr::read_unaligned(offsets_ptr.cast::()) }; - return from_bytes_alloc(base, len, offsets).map(Some); + }; + let Some((base, len)) = data else { + return Ok(None); + }; + if len < size_of::() + TRAILER.len() { + bun_core::debug_warn!("bun standalone module graph is too small to be valid"); + return Ok(None); } - - #[cfg(not(any( - target_os = "macos", - windows, - target_os = "linux", - target_os = "android", - target_os = "freebsd" - )))] - { - unreachable!() + // SAFETY: `[len - Offsets - TRAILER, len)` is in-bounds (checked above) and + // read-only; build short-lived views via raw `read_unaligned` so no `&[u8]` + // ever spans the writable bytecode region carried in `base`'s provenance. + let offsets_ptr = unsafe { base.add(len - size_of::() - TRAILER.len()) }; + // SAFETY: `[len - TRAILER.len(), len)` is in-bounds (length checked above) and read-only. + let trailer_bytes = + unsafe { core::slice::from_raw_parts(base.add(len - TRAILER.len()), TRAILER.len()) }; + if trailer_bytes != TRAILER { + bun_core::debug_warn!("bun standalone module graph has invalid trailer"); + return Ok(None); } + // SAFETY: offsets_ptr has at least size_of::() bytes. + let offsets: Offsets = unsafe { core::ptr::read_unaligned(offsets_ptr.cast::()) }; + from_bytes_alloc(base, len, offsets).map(Some) } /// Hint to the kernel that the embedded `__BUN`/`.bun` source pages are diff --git a/src/sys/windows/mod.rs b/src/sys/windows/mod.rs index e00a3b57b020..e08d3f69ce35 100644 --- a/src/sys/windows/mod.rs +++ b/src/sys/windows/mod.rs @@ -407,8 +407,8 @@ pub use bun_windows_sys::externs::SaferiIsExecutableFileType; /// Codes from . /// Canonical newtype lives in `bun_windows_sys` (tier-0); re-exported here so /// `bun_sys::windows::Win32Error` and `bun_errno::Win32Error` are one nominal -/// type. The full MS-ERREF const table below is parked behind `#[cfg(any())]` -/// — only the subset on `bun_windows_sys::Win32Error` is referenced. +/// type. Only the subset of MS-ERREF consts defined on +/// `bun_windows_sys::Win32Error` is referenced; add new consts there as needed. pub use bun_windows_sys::Win32Error; /// `to_system_errno()` / `to_e()` — extension trait from `bun_errno` (the @@ -434,2816 +434,6 @@ impl Win32ErrorUnwrap for Win32Error { } } -// ────────────────────────────────────────────────────────────────────────── -// DEAD: full 1188-variant MS-ERREF const table. Kept gated for -// reference; move individual consts up into `bun_windows_sys::Win32Error` -// if a new caller needs one. (Inherent impl on a foreign type is illegal, -// so this block cannot be un-gated as-is.) -// ────────────────────────────────────────────────────────────────────────── -#[cfg(any())] -mod _win32error_full_table { - use super::Win32Error; - impl Win32Error { - /// The operation completed successfully. - pub const SUCCESS: Win32Error = Win32Error(0); - /// Incorrect function. - pub const INVALID_FUNCTION: Win32Error = Win32Error(1); - /// The system cannot find the file specified. - pub const FILE_NOT_FOUND: Win32Error = Win32Error(2); - /// The system cannot find the path specified. - pub const PATH_NOT_FOUND: Win32Error = Win32Error(3); - /// The system cannot open the file. - pub const TOO_MANY_OPEN_FILES: Win32Error = Win32Error(4); - /// Access is denied. - pub const ACCESS_DENIED: Win32Error = Win32Error(5); - /// The handle is invalid. - pub const INVALID_HANDLE: Win32Error = Win32Error(6); - /// The storage control blocks were destroyed. - pub const ARENA_TRASHED: Win32Error = Win32Error(7); - /// Not enough storage is available to process this command. - pub const NOT_ENOUGH_MEMORY: Win32Error = Win32Error(8); - /// The storage control block address is invalid. - pub const INVALID_BLOCK: Win32Error = Win32Error(9); - /// The environment is incorrect. - pub const BAD_ENVIRONMENT: Win32Error = Win32Error(10); - /// An attempt was made to load a program with an incorrect format. - pub const BAD_FORMAT: Win32Error = Win32Error(11); - /// The access code is invalid. - pub const INVALID_ACCESS: Win32Error = Win32Error(12); - /// The data is invalid. - pub const INVALID_DATA: Win32Error = Win32Error(13); - /// Not enough storage is available to complete this operation. - pub const OUTOFMEMORY: Win32Error = Win32Error(14); - /// The system cannot find the drive specified. - pub const INVALID_DRIVE: Win32Error = Win32Error(15); - /// The directory cannot be removed. - pub const CURRENT_DIRECTORY: Win32Error = Win32Error(16); - /// The system cannot move the file to a different disk drive. - pub const NOT_SAME_DEVICE: Win32Error = Win32Error(17); - /// There are no more files. - pub const NO_MORE_FILES: Win32Error = Win32Error(18); - /// The media is write protected. - pub const WRITE_PROTECT: Win32Error = Win32Error(19); - /// The system cannot find the device specified. - pub const BAD_UNIT: Win32Error = Win32Error(20); - /// The device is not ready. - pub const NOT_READY: Win32Error = Win32Error(21); - /// The device does not recognize the command. - pub const BAD_COMMAND: Win32Error = Win32Error(22); - /// Data error (cyclic redundancy check). - pub const CRC: Win32Error = Win32Error(23); - /// The program issued a command but the command length is incorrect. - pub const BAD_LENGTH: Win32Error = Win32Error(24); - /// The drive cannot locate a specific area or track on the disk. - pub const SEEK: Win32Error = Win32Error(25); - /// The specified disk or diskette cannot be accessed. - pub const NOT_DOS_DISK: Win32Error = Win32Error(26); - /// The drive cannot find the sector requested. - pub const SECTOR_NOT_FOUND: Win32Error = Win32Error(27); - /// The printer is out of paper. - pub const OUT_OF_PAPER: Win32Error = Win32Error(28); - /// The system cannot write to the specified device. - pub const WRITE_FAULT: Win32Error = Win32Error(29); - /// The system cannot read from the specified device. - pub const READ_FAULT: Win32Error = Win32Error(30); - /// A device attached to the system is not functioning. - pub const GEN_FAILURE: Win32Error = Win32Error(31); - /// The process cannot access the file because it is being used by another process. - pub const SHARING_VIOLATION: Win32Error = Win32Error(32); - /// The process cannot access the file because another process has locked a portion of the file. - pub const LOCK_VIOLATION: Win32Error = Win32Error(33); - /// The wrong diskette is in the drive. - /// Insert %2 (Volume Serial Number: %3) into drive %1. - pub const WRONG_DISK: Win32Error = Win32Error(34); - /// Too many files opened for sharing. - pub const SHARING_BUFFER_EXCEEDED: Win32Error = Win32Error(36); - /// Reached the end of the file. - pub const HANDLE_EOF: Win32Error = Win32Error(38); - /// The disk is full. - pub const HANDLE_DISK_FULL: Win32Error = Win32Error(39); - /// The request is not supported. - pub const NOT_SUPPORTED: Win32Error = Win32Error(50); - /// Windows cannot find the network path. - /// Verify that the network path is correct and the destination computer is not busy or turned off. - /// If Windows still cannot find the network path, contact your network administrator. - pub const REM_NOT_LIST: Win32Error = Win32Error(51); - /// You were not connected because a duplicate name exists on the network. - /// If joining a domain, go to System in Control Panel to change the computer name and try again. - /// If joining a workgroup, choose another workgroup name. - pub const DUP_NAME: Win32Error = Win32Error(52); - /// The network path was not found. - pub const BAD_NETPATH: Win32Error = Win32Error(53); - /// The network is busy. - pub const NETWORK_BUSY: Win32Error = Win32Error(54); - /// The specified network resource or device is no longer available. - pub const DEV_NOT_EXIST: Win32Error = Win32Error(55); - /// The network BIOS command limit has been reached. - pub const TOO_MANY_CMDS: Win32Error = Win32Error(56); - /// A network adapter hardware error occurred. - pub const ADAP_HDW_ERR: Win32Error = Win32Error(57); - /// The specified server cannot perform the requested operation. - pub const BAD_NET_RESP: Win32Error = Win32Error(58); - /// An unexpected network error occurred. - pub const UNEXP_NET_ERR: Win32Error = Win32Error(59); - /// The remote adapter is not compatible. - pub const BAD_REM_ADAP: Win32Error = Win32Error(60); - /// The printer queue is full. - pub const PRINTQ_FULL: Win32Error = Win32Error(61); - /// Space to store the file waiting to be printed is not available on the server. - pub const NO_SPOOL_SPACE: Win32Error = Win32Error(62); - /// Your file waiting to be printed was deleted. - pub const PRINT_CANCELLED: Win32Error = Win32Error(63); - /// The specified network name is no longer available. - pub const NETNAME_DELETED: Win32Error = Win32Error(64); - /// Network access is denied. - pub const NETWORK_ACCESS_DENIED: Win32Error = Win32Error(65); - /// The network resource type is not correct. - pub const BAD_DEV_TYPE: Win32Error = Win32Error(66); - /// The network name cannot be found. - pub const BAD_NET_NAME: Win32Error = Win32Error(67); - /// The name limit for the local computer network adapter card was exceeded. - pub const TOO_MANY_NAMES: Win32Error = Win32Error(68); - /// The network BIOS session limit was exceeded. - pub const TOO_MANY_SESS: Win32Error = Win32Error(69); - /// The remote server has been paused or is in the process of being started. - pub const SHARING_PAUSED: Win32Error = Win32Error(70); - /// No more connections can be made to this remote computer at this time because there are already as many connections as the computer can accept. - pub const REQ_NOT_ACCEP: Win32Error = Win32Error(71); - /// The specified printer or disk device has been paused. - pub const REDIR_PAUSED: Win32Error = Win32Error(72); - /// The file exists. - pub const FILE_EXISTS: Win32Error = Win32Error(80); - /// The directory or file cannot be created. - pub const CANNOT_MAKE: Win32Error = Win32Error(82); - /// Fail on INT 24. - pub const FAIL_I24: Win32Error = Win32Error(83); - /// Storage to process this request is not available. - pub const OUT_OF_STRUCTURES: Win32Error = Win32Error(84); - /// The local device name is already in use. - pub const ALREADY_ASSIGNED: Win32Error = Win32Error(85); - /// The specified network password is not correct. - pub const INVALID_PASSWORD: Win32Error = Win32Error(86); - /// The parameter is incorrect. - pub const INVALID_PARAMETER: Win32Error = Win32Error(87); - /// A write fault occurred on the network. - pub const NET_WRITE_FAULT: Win32Error = Win32Error(88); - /// The system cannot start another process at this time. - pub const NO_PROC_SLOTS: Win32Error = Win32Error(89); - /// Cannot create another system semaphore. - pub const TOO_MANY_SEMAPHORES: Win32Error = Win32Error(100); - /// The exclusive semaphore is owned by another process. - pub const EXCL_SEM_ALREADY_OWNED: Win32Error = Win32Error(101); - /// The semaphore is set and cannot be closed. - pub const SEM_IS_SET: Win32Error = Win32Error(102); - /// The semaphore cannot be set again. - pub const TOO_MANY_SEM_REQUESTS: Win32Error = Win32Error(103); - /// Cannot request exclusive semaphores at interrupt time. - pub const INVALID_AT_INTERRUPT_TIME: Win32Error = Win32Error(104); - /// The previous ownership of this semaphore has ended. - pub const SEM_OWNER_DIED: Win32Error = Win32Error(105); - /// Insert the diskette for drive %1. - pub const SEM_USER_LIMIT: Win32Error = Win32Error(106); - /// The program stopped because an alternate diskette was not inserted. - pub const DISK_CHANGE: Win32Error = Win32Error(107); - /// The disk is in use or locked by another process. - pub const DRIVE_LOCKED: Win32Error = Win32Error(108); - /// The pipe has been ended. - pub const BROKEN_PIPE: Win32Error = Win32Error(109); - /// The system cannot open the device or file specified. - pub const OPEN_FAILED: Win32Error = Win32Error(110); - /// The file name is too long. - pub const BUFFER_OVERFLOW: Win32Error = Win32Error(111); - /// There is not enough space on the disk. - pub const DISK_FULL: Win32Error = Win32Error(112); - /// No more internal file identifiers available. - pub const NO_MORE_SEARCH_HANDLES: Win32Error = Win32Error(113); - /// The target internal file identifier is incorrect. - pub const INVALID_TARGET_HANDLE: Win32Error = Win32Error(114); - /// The IOCTL call made by the application program is not correct. - pub const INVALID_CATEGORY: Win32Error = Win32Error(117); - /// The verify-on-write switch parameter value is not correct. - pub const INVALID_VERIFY_SWITCH: Win32Error = Win32Error(118); - /// The system does not support the command requested. - pub const BAD_DRIVER_LEVEL: Win32Error = Win32Error(119); - /// This function is not supported on this system. - pub const CALL_NOT_IMPLEMENTED: Win32Error = Win32Error(120); - /// The semaphore timeout period has expired. - pub const SEM_TIMEOUT: Win32Error = Win32Error(121); - /// The data area passed to a system call is too small. - pub const INSUFFICIENT_BUFFER: Win32Error = Win32Error(122); - /// The filename, directory name, or volume label syntax is incorrect. - pub const INVALID_NAME: Win32Error = Win32Error(123); - /// The system call level is not correct. - pub const INVALID_LEVEL: Win32Error = Win32Error(124); - /// The disk has no volume label. - pub const NO_VOLUME_LABEL: Win32Error = Win32Error(125); - /// The specified module could not be found. - pub const MOD_NOT_FOUND: Win32Error = Win32Error(126); - /// The specified procedure could not be found. - pub const PROC_NOT_FOUND: Win32Error = Win32Error(127); - /// There are no child processes to wait for. - pub const WAIT_NO_CHILDREN: Win32Error = Win32Error(128); - /// The %1 application cannot be run in Win32 mode. - pub const CHILD_NOT_COMPLETE: Win32Error = Win32Error(129); - /// Attempt to use a file handle to an open disk partition for an operation other than raw disk I/O. - pub const DIRECT_ACCESS_HANDLE: Win32Error = Win32Error(130); - /// An attempt was made to move the file pointer before the beginning of the file. - pub const NEGATIVE_SEEK: Win32Error = Win32Error(131); - /// The file pointer cannot be set on the specified device or file. - pub const SEEK_ON_DEVICE: Win32Error = Win32Error(132); - /// A JOIN or SUBST command cannot be used for a drive that contains previously joined drives. - pub const IS_JOIN_TARGET: Win32Error = Win32Error(133); - /// An attempt was made to use a JOIN or SUBST command on a drive that has already been joined. - pub const IS_JOINED: Win32Error = Win32Error(134); - /// An attempt was made to use a JOIN or SUBST command on a drive that has already been substituted. - pub const IS_SUBSTED: Win32Error = Win32Error(135); - /// The system tried to delete the JOIN of a drive that is not joined. - pub const NOT_JOINED: Win32Error = Win32Error(136); - /// The system tried to delete the substitution of a drive that is not substituted. - pub const NOT_SUBSTED: Win32Error = Win32Error(137); - /// The system tried to join a drive to a directory on a joined drive. - pub const JOIN_TO_JOIN: Win32Error = Win32Error(138); - /// The system tried to substitute a drive to a directory on a substituted drive. - pub const SUBST_TO_SUBST: Win32Error = Win32Error(139); - /// The system tried to join a drive to a directory on a substituted drive. - pub const JOIN_TO_SUBST: Win32Error = Win32Error(140); - /// The system tried to SUBST a drive to a directory on a joined drive. - pub const SUBST_TO_JOIN: Win32Error = Win32Error(141); - /// The system cannot perform a JOIN or SUBST at this time. - pub const BUSY_DRIVE: Win32Error = Win32Error(142); - /// The system cannot join or substitute a drive to or for a directory on the same drive. - pub const SAME_DRIVE: Win32Error = Win32Error(143); - /// The directory is not a subdirectory of the root directory. - pub const DIR_NOT_ROOT: Win32Error = Win32Error(144); - /// The directory is not empty. - pub const DIR_NOT_EMPTY: Win32Error = Win32Error(145); - /// The path specified is being used in a substitute. - pub const IS_SUBST_PATH: Win32Error = Win32Error(146); - /// Not enough resources are available to process this command. - pub const IS_JOIN_PATH: Win32Error = Win32Error(147); - /// The path specified cannot be used at this time. - pub const PATH_BUSY: Win32Error = Win32Error(148); - /// An attempt was made to join or substitute a drive for which a directory on the drive is the target of a previous substitute. - pub const IS_SUBST_TARGET: Win32Error = Win32Error(149); - /// System trace information was not specified in your CONFIG.SYS file, or tracing is disallowed. - pub const SYSTEM_TRACE: Win32Error = Win32Error(150); - /// The number of specified semaphore events for DosMuxSemWait is not correct. - pub const INVALID_EVENT_COUNT: Win32Error = Win32Error(151); - /// DosMuxSemWait did not execute; too many semaphores are already set. - pub const TOO_MANY_MUXWAITERS: Win32Error = Win32Error(152); - /// The DosMuxSemWait list is not correct. - pub const INVALID_LIST_FORMAT: Win32Error = Win32Error(153); - /// The volume label you entered exceeds the label character limit of the target file system. - pub const LABEL_TOO_LONG: Win32Error = Win32Error(154); - /// Cannot create another thread. - pub const TOO_MANY_TCBS: Win32Error = Win32Error(155); - /// The recipient process has refused the signal. - pub const SIGNAL_REFUSED: Win32Error = Win32Error(156); - /// The segment is already discarded and cannot be locked. - pub const DISCARDED: Win32Error = Win32Error(157); - /// The segment is already unlocked. - pub const NOT_LOCKED: Win32Error = Win32Error(158); - /// The address for the thread ID is not correct. - pub const BAD_THREADID_ADDR: Win32Error = Win32Error(159); - /// One or more arguments are not correct. - pub const BAD_ARGUMENTS: Win32Error = Win32Error(160); - /// The specified path is invalid. - pub const BAD_PATHNAME: Win32Error = Win32Error(161); - /// A signal is already pending. - pub const SIGNAL_PENDING: Win32Error = Win32Error(162); - /// No more threads can be created in the system. - pub const MAX_THRDS_REACHED: Win32Error = Win32Error(164); - /// Unable to lock a region of a file. - pub const LOCK_FAILED: Win32Error = Win32Error(167); - /// The requested resource is in use. - pub const BUSY: Win32Error = Win32Error(170); - /// Device's command support detection is in progress. - pub const DEVICE_SUPPORT_IN_PROGRESS: Win32Error = Win32Error(171); - /// A lock request was not outstanding for the supplied cancel region. - pub const CANCEL_VIOLATION: Win32Error = Win32Error(173); - /// The file system does not support atomic changes to the lock type. - pub const ATOMIC_LOCKS_NOT_SUPPORTED: Win32Error = Win32Error(174); - /// The system detected a segment number that was not correct. - pub const INVALID_SEGMENT_NUMBER: Win32Error = Win32Error(180); - /// The operating system cannot run %1. - pub const INVALID_ORDINAL: Win32Error = Win32Error(182); - /// Cannot create a file when that file already exists. - pub const ALREADY_EXISTS: Win32Error = Win32Error(183); - /// The flag passed is not correct. - pub const INVALID_FLAG_NUMBER: Win32Error = Win32Error(186); - /// The specified system semaphore name was not found. - pub const SEM_NOT_FOUND: Win32Error = Win32Error(187); - /// The operating system cannot run %1. - pub const INVALID_STARTING_CODESEG: Win32Error = Win32Error(188); - /// The operating system cannot run %1. - pub const INVALID_STACKSEG: Win32Error = Win32Error(189); - /// The operating system cannot run %1. - pub const INVALID_MODULETYPE: Win32Error = Win32Error(190); - /// Cannot run %1 in Win32 mode. - pub const INVALID_EXE_SIGNATURE: Win32Error = Win32Error(191); - /// The operating system cannot run %1. - pub const EXE_MARKED_INVALID: Win32Error = Win32Error(192); - /// %1 is not a valid Win32 application. - pub const BAD_EXE_FORMAT: Win32Error = Win32Error(193); - /// The operating system cannot run %1. - pub const ITERATED_DATA_EXCEEDS_64k: Win32Error = Win32Error(194); - /// The operating system cannot run %1. - pub const INVALID_MINALLOCSIZE: Win32Error = Win32Error(195); - /// The operating system cannot run this application program. - pub const DYNLINK_FROM_INVALID_RING: Win32Error = Win32Error(196); - /// The operating system is not presently configured to run this application. - pub const IOPL_NOT_ENABLED: Win32Error = Win32Error(197); - /// The operating system cannot run %1. - pub const INVALID_SEGDPL: Win32Error = Win32Error(198); - /// The operating system cannot run this application program. - pub const AUTODATASEG_EXCEEDS_64k: Win32Error = Win32Error(199); - /// The code segment cannot be greater than or equal to 64K. - pub const RING2SEG_MUST_BE_MOVABLE: Win32Error = Win32Error(200); - /// The operating system cannot run %1. - pub const RELOC_CHAIN_XEEDS_SEGLIM: Win32Error = Win32Error(201); - /// The operating system cannot run %1. - pub const INFLOOP_IN_RELOC_CHAIN: Win32Error = Win32Error(202); - /// The system could not find the environment option that was entered. - pub const ENVVAR_NOT_FOUND: Win32Error = Win32Error(203); - /// No process in the command subtree has a signal handler. - pub const NO_SIGNAL_SENT: Win32Error = Win32Error(205); - /// The filename or extension is too long. - pub const FILENAME_EXCED_RANGE: Win32Error = Win32Error(206); - /// The ring 2 stack is in use. - pub const RING2_STACK_IN_USE: Win32Error = Win32Error(207); - /// The global filename characters, * or ?, are entered incorrectly or too many global filename characters are specified. - pub const META_EXPANSION_TOO_LONG: Win32Error = Win32Error(208); - /// The signal being posted is not correct. - pub const INVALID_SIGNAL_NUMBER: Win32Error = Win32Error(209); - /// The signal handler cannot be set. - pub const THREAD_1_INACTIVE: Win32Error = Win32Error(210); - /// The segment is locked and cannot be reallocated. - pub const LOCKED: Win32Error = Win32Error(212); - /// Too many dynamic-link modules are attached to this program or dynamic-link module. - pub const TOO_MANY_MODULES: Win32Error = Win32Error(214); - /// Cannot nest calls to LoadModule. - pub const NESTING_NOT_ALLOWED: Win32Error = Win32Error(215); - /// This version of %1 is not compatible with the version of Windows you're running. - /// Check your computer's system information and then contact the software publisher. - pub const EXE_MACHINE_TYPE_MISMATCH: Win32Error = Win32Error(216); - /// The image file %1 is signed, unable to modify. - pub const EXE_CANNOT_MODIFY_SIGNED_BINARY: Win32Error = Win32Error(217); - /// The image file %1 is strong signed, unable to modify. - pub const EXE_CANNOT_MODIFY_STRONG_SIGNED_BINARY: Win32Error = Win32Error(218); - /// This file is checked out or locked for editing by another user. - pub const FILE_CHECKED_OUT: Win32Error = Win32Error(220); - /// The file must be checked out before saving changes. - pub const CHECKOUT_REQUIRED: Win32Error = Win32Error(221); - /// The file type being saved or retrieved has been blocked. - pub const BAD_FILE_TYPE: Win32Error = Win32Error(222); - /// The file size exceeds the limit allowed and cannot be saved. - pub const FILE_TOO_LARGE: Win32Error = Win32Error(223); - /// Access Denied. Before opening files in this location, you must first add the web site to your trusted sites list, browse to the web site, and select the option to login automatically. - pub const FORMS_AUTH_REQUIRED: Win32Error = Win32Error(224); - /// Operation did not complete successfully because the file contains a virus or potentially unwanted software. - pub const VIRUS_INFECTED: Win32Error = Win32Error(225); - /// This file contains a virus or potentially unwanted software and cannot be opened. - /// Due to the nature of this virus or potentially unwanted software, the file has been removed from this location. - pub const VIRUS_DELETED: Win32Error = Win32Error(226); - /// The pipe is local. - pub const PIPE_LOCAL: Win32Error = Win32Error(229); - /// The pipe state is invalid. - pub const BAD_PIPE: Win32Error = Win32Error(230); - /// All pipe instances are busy. - pub const PIPE_BUSY: Win32Error = Win32Error(231); - /// The pipe is being closed. - pub const NO_DATA: Win32Error = Win32Error(232); - /// No process is on the other end of the pipe. - pub const PIPE_NOT_CONNECTED: Win32Error = Win32Error(233); - /// More data is available. - pub const MORE_DATA: Win32Error = Win32Error(234); - /// The session was canceled. - pub const VC_DISCONNECTED: Win32Error = Win32Error(240); - /// The specified extended attribute name was invalid. - pub const INVALID_EA_NAME: Win32Error = Win32Error(254); - /// The extended attributes are inconsistent. - pub const EA_LIST_INCONSISTENT: Win32Error = Win32Error(255); - /// The wait operation timed out. - pub const IMEOUT: Win32Error = Win32Error(258); - /// No more data is available. - pub const NO_MORE_ITEMS: Win32Error = Win32Error(259); - /// The copy functions cannot be used. - pub const CANNOT_COPY: Win32Error = Win32Error(266); - /// The directory name is invalid. - pub const DIRECTORY: Win32Error = Win32Error(267); - /// The extended attributes did not fit in the buffer. - pub const EAS_DIDNT_FIT: Win32Error = Win32Error(275); - /// The extended attribute file on the mounted file system is corrupt. - pub const EA_FILE_CORRUPT: Win32Error = Win32Error(276); - /// The extended attribute table file is full. - pub const EA_TABLE_FULL: Win32Error = Win32Error(277); - /// The specified extended attribute handle is invalid. - pub const INVALID_EA_HANDLE: Win32Error = Win32Error(278); - /// The mounted file system does not support extended attributes. - pub const EAS_NOT_SUPPORTED: Win32Error = Win32Error(282); - /// Attempt to release mutex not owned by caller. - pub const NOT_OWNER: Win32Error = Win32Error(288); - /// Too many posts were made to a semaphore. - pub const TOO_MANY_POSTS: Win32Error = Win32Error(298); - /// Only part of a ReadProcessMemory or WriteProcessMemory request was completed. - pub const PARTIAL_COPY: Win32Error = Win32Error(299); - /// The oplock request is denied. - pub const OPLOCK_NOT_GRANTED: Win32Error = Win32Error(300); - /// An invalid oplock acknowledgment was received by the system. - pub const INVALID_OPLOCK_PROTOCOL: Win32Error = Win32Error(301); - /// The volume is too fragmented to complete this operation. - pub const DISK_TOO_FRAGMENTED: Win32Error = Win32Error(302); - /// The file cannot be opened because it is in the process of being deleted. - pub const DELETE_PENDING: Win32Error = Win32Error(303); - /// Short name settings may not be changed on this volume due to the global registry setting. - pub const INCOMPATIBLE_WITH_GLOBAL_SHORT_NAME_REGISTRY_SETTING: Win32Error = - Win32Error(304); - /// Short names are not enabled on this volume. - pub const SHORT_NAMES_NOT_ENABLED_ON_VOLUME: Win32Error = Win32Error(305); - /// The security stream for the given volume is in an inconsistent state. Please run CHKDSK on the volume. - pub const SECURITY_STREAM_IS_INCONSISTENT: Win32Error = Win32Error(306); - /// A requested file lock operation cannot be processed due to an invalid byte range. - pub const INVALID_LOCK_RANGE: Win32Error = Win32Error(307); - /// The subsystem needed to support the image type is not present. - pub const IMAGE_SUBSYSTEM_NOT_PRESENT: Win32Error = Win32Error(308); - /// The specified file already has a notification GUID associated with it. - pub const NOTIFICATION_GUID_ALREADY_DEFINED: Win32Error = Win32Error(309); - /// An invalid exception handler routine has been detected. - pub const INVALID_EXCEPTION_HANDLER: Win32Error = Win32Error(310); - /// Duplicate privileges were specified for the token. - pub const DUPLICATE_PRIVILEGES: Win32Error = Win32Error(311); - /// No ranges for the specified operation were able to be processed. - pub const NO_RANGES_PROCESSED: Win32Error = Win32Error(312); - /// Operation is not allowed on a file system internal file. - pub const NOT_ALLOWED_ON_SYSTEM_FILE: Win32Error = Win32Error(313); - /// The physical resources of this disk have been exhausted. - pub const DISK_RESOURCES_EXHAUSTED: Win32Error = Win32Error(314); - /// The token representing the data is invalid. - pub const INVALID_TOKEN: Win32Error = Win32Error(315); - /// The device does not support the command feature. - pub const DEVICE_FEATURE_NOT_SUPPORTED: Win32Error = Win32Error(316); - /// The system cannot find message text for message number 0x%1 in the message file for %2. - pub const MR_MID_NOT_FOUND: Win32Error = Win32Error(317); - /// The scope specified was not found. - pub const SCOPE_NOT_FOUND: Win32Error = Win32Error(318); - /// The Central Access Policy specified is not defined on the target machine. - pub const UNDEFINED_SCOPE: Win32Error = Win32Error(319); - /// The Central Access Policy obtained from Active Directory is invalid. - pub const INVALID_CAP: Win32Error = Win32Error(320); - /// The device is unreachable. - pub const DEVICE_UNREACHABLE: Win32Error = Win32Error(321); - /// The target device has insufficient resources to complete the operation. - pub const DEVICE_NO_RESOURCES: Win32Error = Win32Error(322); - /// A data integrity checksum error occurred. Data in the file stream is corrupt. - pub const DATA_CHECKSUM_ERROR: Win32Error = Win32Error(323); - /// An attempt was made to modify both a KERNEL and normal Extended Attribute (EA) in the same operation. - pub const INTERMIXED_KERNEL_EA_OPERATION: Win32Error = Win32Error(324); - /// Device does not support file-level TRIM. - pub const FILE_LEVEL_TRIM_NOT_SUPPORTED: Win32Error = Win32Error(326); - /// The command specified a data offset that does not align to the device's granularity/alignment. - pub const OFFSET_ALIGNMENT_VIOLATION: Win32Error = Win32Error(327); - /// The command specified an invalid field in its parameter list. - pub const INVALID_FIELD_IN_PARAMETER_LIST: Win32Error = Win32Error(328); - /// An operation is currently in progress with the device. - pub const OPERATION_IN_PROGRESS: Win32Error = Win32Error(329); - /// An attempt was made to send down the command via an invalid path to the target device. - pub const BAD_DEVICE_PATH: Win32Error = Win32Error(330); - /// The command specified a number of descriptors that exceeded the maximum supported by the device. - pub const TOO_MANY_DESCRIPTORS: Win32Error = Win32Error(331); - /// Scrub is disabled on the specified file. - pub const SCRUB_DATA_DISABLED: Win32Error = Win32Error(332); - /// The storage device does not provide redundancy. - pub const NOT_REDUNDANT_STORAGE: Win32Error = Win32Error(333); - /// An operation is not supported on a resident file. - pub const RESIDENT_FILE_NOT_SUPPORTED: Win32Error = Win32Error(334); - /// An operation is not supported on a compressed file. - pub const COMPRESSED_FILE_NOT_SUPPORTED: Win32Error = Win32Error(335); - /// An operation is not supported on a directory. - pub const DIRECTORY_NOT_SUPPORTED: Win32Error = Win32Error(336); - /// The specified copy of the requested data could not be read. - pub const NOT_READ_FROM_COPY: Win32Error = Win32Error(337); - /// No action was taken as a system reboot is required. - pub const FAIL_NOACTION_REBOOT: Win32Error = Win32Error(350); - /// The shutdown operation failed. - pub const FAIL_SHUTDOWN: Win32Error = Win32Error(351); - /// The restart operation failed. - pub const FAIL_RESTART: Win32Error = Win32Error(352); - /// The maximum number of sessions has been reached. - pub const MAX_SESSIONS_REACHED: Win32Error = Win32Error(353); - /// The thread is already in background processing mode. - pub const THREAD_MODE_ALREADY_BACKGROUND: Win32Error = Win32Error(400); - /// The thread is not in background processing mode. - pub const THREAD_MODE_NOT_BACKGROUND: Win32Error = Win32Error(401); - /// The process is already in background processing mode. - pub const PROCESS_MODE_ALREADY_BACKGROUND: Win32Error = Win32Error(402); - /// The process is not in background processing mode. - pub const PROCESS_MODE_NOT_BACKGROUND: Win32Error = Win32Error(403); - /// Attempt to access invalid address. - pub const INVALID_ADDRESS: Win32Error = Win32Error(487); - /// User profile cannot be loaded. - pub const USER_PROFILE_LOAD: Win32Error = Win32Error(500); - /// Arithmetic result exceeded 32 bits. - pub const ARITHMETIC_OVERFLOW: Win32Error = Win32Error(534); - /// There is a process on other end of the pipe. - pub const PIPE_CONNECTED: Win32Error = Win32Error(535); - /// Waiting for a process to open the other end of the pipe. - pub const PIPE_LISTENING: Win32Error = Win32Error(536); - /// Application verifier has found an error in the current process. - pub const VERIFIER_STOP: Win32Error = Win32Error(537); - /// An error occurred in the ABIOS subsystem. - pub const ABIOS_ERROR: Win32Error = Win32Error(538); - /// A warning occurred in the WX86 subsystem. - pub const WX86_WARNING: Win32Error = Win32Error(539); - /// An error occurred in the WX86 subsystem. - pub const WX86_ERROR: Win32Error = Win32Error(540); - /// An attempt was made to cancel or set a timer that has an associated APC and the subject thread is not the thread that originally set the timer with an associated APC routine. - pub const TIMER_NOT_CANCELED: Win32Error = Win32Error(541); - /// Unwind exception code. - pub const UNWIND: Win32Error = Win32Error(542); - /// An invalid or unaligned stack was encountered during an unwind operation. - pub const BAD_STACK: Win32Error = Win32Error(543); - /// An invalid unwind target was encountered during an unwind operation. - pub const INVALID_UNWIND_TARGET: Win32Error = Win32Error(544); - /// Invalid Object Attributes specified to NtCreatePort or invalid Port Attributes specified to NtConnectPort - pub const INVALID_PORT_ATTRIBUTES: Win32Error = Win32Error(545); - /// Length of message passed to NtRequestPort or NtRequestWaitReplyPort was longer than the maximum message allowed by the port. - pub const PORT_MESSAGE_TOO_LONG: Win32Error = Win32Error(546); - /// An attempt was made to lower a quota limit below the current usage. - pub const INVALID_QUOTA_LOWER: Win32Error = Win32Error(547); - /// An attempt was made to attach to a device that was already attached to another device. - pub const DEVICE_ALREADY_ATTACHED: Win32Error = Win32Error(548); - /// An attempt was made to execute an instruction at an unaligned address and the host system does not support unaligned instruction references. - pub const INSTRUCTION_MISALIGNMENT: Win32Error = Win32Error(549); - /// Profiling not started. - pub const PROFILING_NOT_STARTED: Win32Error = Win32Error(550); - /// Profiling not stopped. - pub const PROFILING_NOT_STOPPED: Win32Error = Win32Error(551); - /// The passed ACL did not contain the minimum required information. - pub const COULD_NOT_INTERPRET: Win32Error = Win32Error(552); - /// The number of active profiling objects is at the maximum and no more may be started. - pub const PROFILING_AT_LIMIT: Win32Error = Win32Error(553); - /// Used to indicate that an operation cannot continue without blocking for I/O. - pub const CANT_WAIT: Win32Error = Win32Error(554); - /// Indicates that a thread attempted to terminate itself by default (called NtTerminateThread with NULL) and it was the last thread in the current process. - pub const CANT_TERMINATE_SELF: Win32Error = Win32Error(555); - /// If an MM error is returned which is not defined in the standard FsRtl filter, it is converted to one of the following errors which is guaranteed to be in the filter. - /// In this case information is lost, however, the filter correctly handles the exception. - pub const UNEXPECTED_MM_CREATE_ERR: Win32Error = Win32Error(556); - /// If an MM error is returned which is not defined in the standard FsRtl filter, it is converted to one of the following errors which is guaranteed to be in the filter. - /// In this case information is lost, however, the filter correctly handles the exception. - pub const UNEXPECTED_MM_MAP_ERROR: Win32Error = Win32Error(557); - /// If an MM error is returned which is not defined in the standard FsRtl filter, it is converted to one of the following errors which is guaranteed to be in the filter. - /// In this case information is lost, however, the filter correctly handles the exception. - pub const UNEXPECTED_MM_EXTEND_ERR: Win32Error = Win32Error(558); - /// A malformed function table was encountered during an unwind operation. - pub const BAD_FUNCTION_TABLE: Win32Error = Win32Error(559); - /// Indicates that an attempt was made to assign protection to a file system file or directory and one of the SIDs in the security descriptor could not be translated into a GUID that could be stored by the file system. - /// This causes the protection attempt to fail, which may cause a file creation attempt to fail. - pub const NO_GUID_TRANSLATION: Win32Error = Win32Error(560); - /// Indicates that an attempt was made to grow an LDT by setting its size, or that the size was not an even number of selectors. - pub const INVALID_LDT_SIZE: Win32Error = Win32Error(561); - /// Indicates that the starting value for the LDT information was not an integral multiple of the selector size. - pub const INVALID_LDT_OFFSET: Win32Error = Win32Error(563); - /// Indicates that the user supplied an invalid descriptor when trying to set up Ldt descriptors. - pub const INVALID_LDT_DESCRIPTOR: Win32Error = Win32Error(564); - /// Indicates a process has too many threads to perform the requested action. - /// For example, assignment of a primary token may only be performed when a process has zero or one threads. - pub const TOO_MANY_THREADS: Win32Error = Win32Error(565); - /// An attempt was made to operate on a thread within a specific process, but the thread specified is not in the process specified. - pub const THREAD_NOT_IN_PROCESS: Win32Error = Win32Error(566); - /// Page file quota was exceeded. - pub const PAGEFILE_QUOTA_EXCEEDED: Win32Error = Win32Error(567); - /// The Netlogon service cannot start because another Netlogon service running in the domain conflicts with the specified role. - pub const LOGON_SERVER_CONFLICT: Win32Error = Win32Error(568); - /// The SAM database on a Windows Server is significantly out of synchronization with the copy on the Domain Controller. A complete synchronization is required. - pub const SYNCHRONIZATION_REQUIRED: Win32Error = Win32Error(569); - /// The NtCreateFile API failed. This error should never be returned to an application, it is a place holder for the Windows Lan Manager Redirector to use in its internal error mapping routines. - pub const NET_OPEN_FAILED: Win32Error = Win32Error(570); - /// {Privilege Failed} The I/O permissions for the process could not be changed. - pub const IO_PRIVILEGE_FAILED: Win32Error = Win32Error(571); - /// {Application Exit by CTRL+C} The application terminated as a result of a CTRL+C. - pub const CONTROL_C_EXIT: Win32Error = Win32Error(572); - /// {Missing System File} The required system file %hs is bad or missing. - pub const MISSING_SYSTEMFILE: Win32Error = Win32Error(573); - /// {Application Error} The exception %s (0x%08lx) occurred in the application at location 0x%08lx. - pub const UNHANDLED_EXCEPTION: Win32Error = Win32Error(574); - /// {Application Error} The application was unable to start correctly (0x%lx). Click OK to close the application. - pub const APP_INIT_FAILURE: Win32Error = Win32Error(575); - /// {Unable to Create Paging File} The creation of the paging file %hs failed (%lx). The requested size was %ld. - pub const PAGEFILE_CREATE_FAILED: Win32Error = Win32Error(576); - /// Windows cannot verify the digital signature for this file. - /// A recent hardware or software change might have installed a file that is signed incorrectly or damaged, or that might be malicious software from an unknown source. - pub const INVALID_IMAGE_HASH: Win32Error = Win32Error(577); - /// {No Paging File Specified} No paging file was specified in the system configuration. - pub const NO_PAGEFILE: Win32Error = Win32Error(578); - /// {EXCEPTION} A real-mode application issued a floating-point instruction and floating-point hardware is not present. - pub const ILLEGAL_FLOAT_CONTEXT: Win32Error = Win32Error(579); - /// An event pair synchronization operation was performed using the thread specific client/server event pair object, but no event pair object was associated with the thread. - pub const NO_EVENT_PAIR: Win32Error = Win32Error(580); - /// A Windows Server has an incorrect configuration. - pub const DOMAIN_CTRLR_CONFIG_ERROR: Win32Error = Win32Error(581); - /// An illegal character was encountered. - /// For a multi-byte character set this includes a lead byte without a succeeding trail byte. - /// For the Unicode character set this includes the characters 0xFFFF and 0xFFFE. - pub const ILLEGAL_CHARACTER: Win32Error = Win32Error(582); - /// The Unicode character is not defined in the Unicode character set installed on the system. - pub const UNDEFINED_CHARACTER: Win32Error = Win32Error(583); - /// The paging file cannot be created on a floppy diskette. - pub const FLOPPY_VOLUME: Win32Error = Win32Error(584); - /// The system BIOS failed to connect a system interrupt to the device or bus for which the device is connected. - pub const BIOS_FAILED_TO_CONNECT_INTERRUPT: Win32Error = Win32Error(585); - /// This operation is only allowed for the Primary Domain Controller of the domain. - pub const BACKUP_CONTROLLER: Win32Error = Win32Error(586); - /// An attempt was made to acquire a mutant such that its maximum count would have been exceeded. - pub const MUTANT_LIMIT_EXCEEDED: Win32Error = Win32Error(587); - /// A volume has been accessed for which a file system driver is required that has not yet been loaded. - pub const FS_DRIVER_REQUIRED: Win32Error = Win32Error(588); - /// {Registry File Failure} The registry cannot load the hive (file): %hs or its log or alternate. It is corrupt, absent, or not writable. - pub const CANNOT_LOAD_REGISTRY_FILE: Win32Error = Win32Error(589); - /// {Unexpected Failure in DebugActiveProcess} An unexpected failure occurred while processing a DebugActiveProcess API request. - /// You may choose OK to terminate the process, or Cancel to ignore the error. - pub const DEBUG_ATTACH_FAILED: Win32Error = Win32Error(590); - /// {Fatal System Error} The %hs system process terminated unexpectedly with a status of 0x%08x (0x%08x 0x%08x). The system has been shut down. - pub const SYSTEM_PROCESS_TERMINATED: Win32Error = Win32Error(591); - /// {Data Not Accepted} The TDI client could not handle the data received during an indication. - pub const DATA_NOT_ACCEPTED: Win32Error = Win32Error(592); - /// NTVDM encountered a hard error. - pub const VDM_HARD_ERROR: Win32Error = Win32Error(593); - /// {Cancel Timeout} The driver %hs failed to complete a cancelled I/O request in the allotted time. - pub const DRIVER_CANCEL_TIMEOUT: Win32Error = Win32Error(594); - /// {Reply Message Mismatch} An attempt was made to reply to an LPC message, but the thread specified by the client ID in the message was not waiting on that message. - pub const REPLY_MESSAGE_MISMATCH: Win32Error = Win32Error(595); - /// {Delayed Write Failed} Windows was unable to save all the data for the file %hs. The data has been lost. - /// This error may be caused by a failure of your computer hardware or network connection. Please try to save this file elsewhere. - pub const LOST_WRITEBEHIND_DATA: Win32Error = Win32Error(596); - /// The parameter(s) passed to the server in the client/server shared memory window were invalid. - /// Too much data may have been put in the shared memory window. - pub const CLIENT_SERVER_PARAMETERS_INVALID: Win32Error = Win32Error(597); - /// The stream is not a tiny stream. - pub const NOT_TINY_STREAM: Win32Error = Win32Error(598); - /// The request must be handled by the stack overflow code. - pub const STACK_OVERFLOW_READ: Win32Error = Win32Error(599); - /// Internal OFS status codes indicating how an allocation operation is handled. - /// Either it is retried after the containing onode is moved or the extent stream is converted to a large stream. - pub const CONVERT_TO_LARGE: Win32Error = Win32Error(600); - /// The attempt to find the object found an object matching by ID on the volume but it is out of the scope of the handle used for the operation. - pub const FOUND_OUT_OF_SCOPE: Win32Error = Win32Error(601); - /// The bucket array must be grown. Retry transaction after doing so. - pub const ALLOCATE_BUCKET: Win32Error = Win32Error(602); - /// The user/kernel marshalling buffer has overflowed. - pub const MARSHALL_OVERFLOW: Win32Error = Win32Error(603); - /// The supplied variant structure contains invalid data. - pub const INVALID_VARIANT: Win32Error = Win32Error(604); - /// The specified buffer contains ill-formed data. - pub const BAD_COMPRESSION_BUFFER: Win32Error = Win32Error(605); - /// {Audit Failed} An attempt to generate a security audit failed. - pub const AUDIT_FAILED: Win32Error = Win32Error(606); - /// The timer resolution was not previously set by the current process. - pub const TIMER_RESOLUTION_NOT_SET: Win32Error = Win32Error(607); - /// There is insufficient account information to log you on. - pub const INSUFFICIENT_LOGON_INFO: Win32Error = Win32Error(608); - /// {Invalid DLL Entrypoint} The dynamic link library %hs is not written correctly. - /// The stack pointer has been left in an inconsistent state. - /// The entrypoint should be declared as WINAPI or STDCALL. - /// Select YES to fail the DLL load. Select NO to continue execution. - /// Selecting NO may cause the application to operate incorrectly. - pub const BAD_DLL_ENTRYPOINT: Win32Error = Win32Error(609); - /// {Invalid Service Callback Entrypoint} The %hs service is not written correctly. - /// The stack pointer has been left in an inconsistent state. - /// The callback entrypoint should be declared as WINAPI or STDCALL. - /// Selecting OK will cause the service to continue operation. - /// However, the service process may operate incorrectly. - pub const BAD_SERVICE_ENTRYPOINT: Win32Error = Win32Error(610); - /// There is an IP address conflict with another system on the network. - pub const IP_ADDRESS_CONFLICT1: Win32Error = Win32Error(611); - /// There is an IP address conflict with another system on the network. - pub const IP_ADDRESS_CONFLICT2: Win32Error = Win32Error(612); - /// {Low On Registry Space} The system has reached the maximum size allowed for the system part of the registry. Additional storage requests will be ignored. - pub const REGISTRY_QUOTA_LIMIT: Win32Error = Win32Error(613); - /// A callback return system service cannot be executed when no callback is active. - pub const NO_CALLBACK_ACTIVE: Win32Error = Win32Error(614); - /// The password provided is too short to meet the policy of your user account. Please choose a longer password. - pub const PWD_TOO_SHORT: Win32Error = Win32Error(615); - /// The policy of your user account does not allow you to change passwords too frequently. - /// This is done to prevent users from changing back to a familiar, but potentially discovered, password. - /// If you feel your password has been compromised then please contact your administrator immediately to have a new one assigned. - pub const PWD_TOO_RECENT: Win32Error = Win32Error(616); - /// You have attempted to change your password to one that you have used in the past. - /// The policy of your user account does not allow this. - /// Please select a password that you have not previously used. - pub const PWD_HISTORY_CONFLICT: Win32Error = Win32Error(617); - /// The specified compression format is unsupported. - pub const UNSUPPORTED_COMPRESSION: Win32Error = Win32Error(618); - /// The specified hardware profile configuration is invalid. - pub const INVALID_HW_PROFILE: Win32Error = Win32Error(619); - /// The specified Plug and Play registry device path is invalid. - pub const INVALID_PLUGPLAY_DEVICE_PATH: Win32Error = Win32Error(620); - /// The specified quota list is internally inconsistent with its descriptor. - pub const QUOTA_LIST_INCONSISTENT: Win32Error = Win32Error(621); - /// {Windows Evaluation Notification} The evaluation period for this installation of Windows has expired. This system will shutdown in 1 hour. - /// To restore access to this installation of Windows, please upgrade this installation using a licensed distribution of this product. - pub const EVALUATION_EXPIRATION: Win32Error = Win32Error(622); - /// {Illegal System DLL Relocation} The system DLL %hs was relocated in memory. The application will not run properly. - /// The relocation occurred because the DLL %hs occupied an address range reserved for Windows system DLLs. - /// The vendor supplying the DLL should be contacted for a new DLL. - pub const ILLEGAL_DLL_RELOCATION: Win32Error = Win32Error(623); - /// {DLL Initialization Failed} The application failed to initialize because the window station is shutting down. - pub const DLL_INIT_FAILED_LOGOFF: Win32Error = Win32Error(624); - /// The validation process needs to continue on to the next step. - pub const VALIDATE_CONTINUE: Win32Error = Win32Error(625); - /// There are no more matches for the current index enumeration. - pub const NO_MORE_MATCHES: Win32Error = Win32Error(626); - /// The range could not be added to the range list because of a conflict. - pub const RANGE_LIST_CONFLICT: Win32Error = Win32Error(627); - /// The server process is running under a SID different than that required by client. - pub const SERVER_SID_MISMATCH: Win32Error = Win32Error(628); - /// A group marked use for deny only cannot be enabled. - pub const CANT_ENABLE_DENY_ONLY: Win32Error = Win32Error(629); - /// {EXCEPTION} Multiple floating point faults. - pub const FLOAT_MULTIPLE_FAULTS: Win32Error = Win32Error(630); - /// {EXCEPTION} Multiple floating point traps. - pub const FLOAT_MULTIPLE_TRAPS: Win32Error = Win32Error(631); - /// The requested interface is not supported. - pub const NOINTERFACE: Win32Error = Win32Error(632); - /// {System Standby Failed} The driver %hs does not support standby mode. - /// Updating this driver may allow the system to go to standby mode. - pub const DRIVER_FAILED_SLEEP: Win32Error = Win32Error(633); - /// The system file %1 has become corrupt and has been replaced. - pub const CORRUPT_SYSTEM_FILE: Win32Error = Win32Error(634); - /// {Virtual Memory Minimum Too Low} Your system is low on virtual memory. - /// Windows is increasing the size of your virtual memory paging file. - /// During this process, memory requests for some applications may be denied. For more information, see Help. - pub const COMMITMENT_MINIMUM: Win32Error = Win32Error(635); - /// A device was removed so enumeration must be restarted. - pub const PNP_RESTART_ENUMERATION: Win32Error = Win32Error(636); - /// {Fatal System Error} The system image %s is not properly signed. - /// The file has been replaced with the signed file. The system has been shut down. - pub const SYSTEM_IMAGE_BAD_SIGNATURE: Win32Error = Win32Error(637); - /// Device will not start without a reboot. - pub const PNP_REBOOT_REQUIRED: Win32Error = Win32Error(638); - /// There is not enough power to complete the requested operation. - pub const INSUFFICIENT_POWER: Win32Error = Win32Error(639); - /// ERROR_MULTIPLE_FAULT_VIOLATION - pub const MULTIPLE_FAULT_VIOLATION: Win32Error = Win32Error(640); - /// The system is in the process of shutting down. - pub const SYSTEM_SHUTDOWN: Win32Error = Win32Error(641); - /// An attempt to remove a processes DebugPort was made, but a port was not already associated with the process. - pub const PORT_NOT_SET: Win32Error = Win32Error(642); - /// This version of Windows is not compatible with the behavior version of directory forest, domain or domain controller. - pub const DS_VERSION_CHECK_FAILURE: Win32Error = Win32Error(643); - /// The specified range could not be found in the range list. - pub const RANGE_NOT_FOUND: Win32Error = Win32Error(644); - /// The driver was not loaded because the system is booting into safe mode. - pub const NOT_SAFE_MODE_DRIVER: Win32Error = Win32Error(646); - /// The driver was not loaded because it failed its initialization call. - pub const FAILED_DRIVER_ENTRY: Win32Error = Win32Error(647); - /// The "%hs" encountered an error while applying power or reading the device configuration. - /// This may be caused by a failure of your hardware or by a poor connection. - pub const DEVICE_ENUMERATION_ERROR: Win32Error = Win32Error(648); - /// The create operation failed because the name contained at least one mount point which resolves to a volume to which the specified device object is not attached. - pub const MOUNT_POINT_NOT_RESOLVED: Win32Error = Win32Error(649); - /// The device object parameter is either not a valid device object or is not attached to the volume specified by the file name. - pub const INVALID_DEVICE_OBJECT_PARAMETER: Win32Error = Win32Error(650); - /// A Machine Check Error has occurred. - /// Please check the system eventlog for additional information. - pub const MCA_OCCURED: Win32Error = Win32Error(651); - /// There was error [%2] processing the driver database. - pub const DRIVER_DATABASE_ERROR: Win32Error = Win32Error(652); - /// System hive size has exceeded its limit. - pub const SYSTEM_HIVE_TOO_LARGE: Win32Error = Win32Error(653); - /// The driver could not be loaded because a previous version of the driver is still in memory. - pub const DRIVER_FAILED_PRIOR_UNLOAD: Win32Error = Win32Error(654); - /// {Volume Shadow Copy Service} Please wait while the Volume Shadow Copy Service prepares volume %hs for hibernation. - pub const VOLSNAP_PREPARE_HIBERNATE: Win32Error = Win32Error(655); - /// The system has failed to hibernate (The error code is %hs). - /// Hibernation will be disabled until the system is restarted. - pub const HIBERNATION_FAILURE: Win32Error = Win32Error(656); - /// The password provided is too long to meet the policy of your user account. Please choose a shorter password. - pub const PWD_TOO_LONG: Win32Error = Win32Error(657); - /// The requested operation could not be completed due to a file system limitation. - pub const FILE_SYSTEM_LIMITATION: Win32Error = Win32Error(665); - /// An assertion failure has occurred. - pub const ASSERTION_FAILURE: Win32Error = Win32Error(668); - /// An error occurred in the ACPI subsystem. - pub const ACPI_ERROR: Win32Error = Win32Error(669); - /// WOW Assertion Error. - pub const WOW_ASSERTION: Win32Error = Win32Error(670); - /// A device is missing in the system BIOS MPS table. This device will not be used. - /// Please contact your system vendor for system BIOS update. - pub const PNP_BAD_MPS_TABLE: Win32Error = Win32Error(671); - /// A translator failed to translate resources. - pub const PNP_TRANSLATION_FAILED: Win32Error = Win32Error(672); - /// A IRQ translator failed to translate resources. - pub const PNP_IRQ_TRANSLATION_FAILED: Win32Error = Win32Error(673); - /// Driver %2 returned invalid ID for a child device (%3). - pub const PNP_INVALID_ID: Win32Error = Win32Error(674); - /// {Kernel Debugger Awakened} the system debugger was awakened by an interrupt. - pub const WAKE_SYSTEM_DEBUGGER: Win32Error = Win32Error(675); - /// {Handles Closed} Handles to objects have been automatically closed as a result of the requested operation. - pub const HANDLES_CLOSED: Win32Error = Win32Error(676); - /// {Too Much Information} The specified access control list (ACL) contained more information than was expected. - pub const EXTRANEOUS_INFORMATION: Win32Error = Win32Error(677); - /// This warning level status indicates that the transaction state already exists for the registry sub-tree, but that a transaction commit was previously aborted. - /// The commit has NOT been completed, but has not been rolled back either (so it may still be committed if desired). - pub const RXACT_COMMIT_NECESSARY: Win32Error = Win32Error(678); - /// {Media Changed} The media may have changed. - pub const MEDIA_CHECK: Win32Error = Win32Error(679); - /// {GUID Substitution} During the translation of a global identifier (GUID) to a Windows security ID (SID), no administratively-defined GUID prefix was found. - /// A substitute prefix was used, which will not compromise system security. - /// However, this may provide a more restrictive access than intended. - pub const GUID_SUBSTITUTION_MADE: Win32Error = Win32Error(680); - /// The create operation stopped after reaching a symbolic link. - pub const STOPPED_ON_SYMLINK: Win32Error = Win32Error(681); - /// A long jump has been executed. - pub const LONGJUMP: Win32Error = Win32Error(682); - /// The Plug and Play query operation was not successful. - pub const PLUGPLAY_QUERY_VETOED: Win32Error = Win32Error(683); - /// A frame consolidation has been executed. - pub const UNWIND_CONSOLIDATE: Win32Error = Win32Error(684); - /// {Registry Hive Recovered} Registry hive (file): %hs was corrupted and it has been recovered. Some data might have been lost. - pub const REGISTRY_HIVE_RECOVERED: Win32Error = Win32Error(685); - /// The application is attempting to run executable code from the module %hs. This may be insecure. - /// An alternative, %hs, is available. Should the application use the secure module %hs? - pub const DLL_MIGHT_BE_INSECURE: Win32Error = Win32Error(686); - /// The application is loading executable code from the module %hs. - /// This is secure, but may be incompatible with previous releases of the operating system. - /// An alternative, %hs, is available. Should the application use the secure module %hs? - pub const DLL_MIGHT_BE_INCOMPATIBLE: Win32Error = Win32Error(687); - /// Debugger did not handle the exception. - pub const DBG_EXCEPTION_NOT_HANDLED: Win32Error = Win32Error(688); - /// Debugger will reply later. - pub const DBG_REPLY_LATER: Win32Error = Win32Error(689); - /// Debugger cannot provide handle. - pub const DBG_UNABLE_TO_PROVIDE_HANDLE: Win32Error = Win32Error(690); - /// Debugger terminated thread. - pub const DBG_TERMINATE_THREAD: Win32Error = Win32Error(691); - /// Debugger terminated process. - pub const DBG_TERMINATE_PROCESS: Win32Error = Win32Error(692); - /// Debugger got control C. - pub const DBG_CONTROL_C: Win32Error = Win32Error(693); - /// Debugger printed exception on control C. - pub const DBG_PRINTEXCEPTION_C: Win32Error = Win32Error(694); - /// Debugger received RIP exception. - pub const DBG_RIPEXCEPTION: Win32Error = Win32Error(695); - /// Debugger received control break. - pub const DBG_CONTROL_BREAK: Win32Error = Win32Error(696); - /// Debugger command communication exception. - pub const DBG_COMMAND_EXCEPTION: Win32Error = Win32Error(697); - /// {Object Exists} An attempt was made to create an object and the object name already existed. - pub const OBJECT_NAME_EXISTS: Win32Error = Win32Error(698); - /// {Thread Suspended} A thread termination occurred while the thread was suspended. - /// The thread was resumed, and termination proceeded. - pub const THREAD_WAS_SUSPENDED: Win32Error = Win32Error(699); - /// {Image Relocated} An image file could not be mapped at the address specified in the image file. Local fixups must be performed on this image. - pub const IMAGE_NOT_AT_BASE: Win32Error = Win32Error(700); - /// This informational level status indicates that a specified registry sub-tree transaction state did not yet exist and had to be created. - pub const RXACT_STATE_CREATED: Win32Error = Win32Error(701); - /// {Segment Load} A virtual DOS machine (VDM) is loading, unloading, or moving an MS-DOS or Win16 program segment image. - /// An exception is raised so a debugger can load, unload or track symbols and breakpoints within these 16-bit segments. - pub const SEGMENT_NOTIFICATION: Win32Error = Win32Error(702); - /// {Invalid Current Directory} The process cannot switch to the startup current directory %hs. - /// Select OK to set current directory to %hs, or select CANCEL to exit. - pub const BAD_CURRENT_DIRECTORY: Win32Error = Win32Error(703); - /// {Redundant Read} To satisfy a read request, the NT fault-tolerant file system successfully read the requested data from a redundant copy. - /// This was done because the file system encountered a failure on a member of the fault-tolerant volume, but was unable to reassign the failing area of the device. - pub const FT_READ_RECOVERY_FROM_BACKUP: Win32Error = Win32Error(704); - /// {Redundant Write} To satisfy a write request, the NT fault-tolerant file system successfully wrote a redundant copy of the information. - /// This was done because the file system encountered a failure on a member of the fault-tolerant volume, but was not able to reassign the failing area of the device. - pub const FT_WRITE_RECOVERY: Win32Error = Win32Error(705); - /// {Machine Type Mismatch} The image file %hs is valid, but is for a machine type other than the current machine. - /// Select OK to continue, or CANCEL to fail the DLL load. - pub const IMAGE_MACHINE_TYPE_MISMATCH: Win32Error = Win32Error(706); - /// {Partial Data Received} The network transport returned partial data to its client. The remaining data will be sent later. - pub const RECEIVE_PARTIAL: Win32Error = Win32Error(707); - /// {Expedited Data Received} The network transport returned data to its client that was marked as expedited by the remote system. - pub const RECEIVE_EXPEDITED: Win32Error = Win32Error(708); - /// {Partial Expedited Data Received} The network transport returned partial data to its client and this data was marked as expedited by the remote system. The remaining data will be sent later. - pub const RECEIVE_PARTIAL_EXPEDITED: Win32Error = Win32Error(709); - /// {TDI Event Done} The TDI indication has completed successfully. - pub const EVENT_DONE: Win32Error = Win32Error(710); - /// {TDI Event Pending} The TDI indication has entered the pending state. - pub const EVENT_PENDING: Win32Error = Win32Error(711); - /// Checking file system on %wZ. - pub const CHECKING_FILE_SYSTEM: Win32Error = Win32Error(712); - /// {Fatal Application Exit} %hs. - pub const FATAL_APP_EXIT: Win32Error = Win32Error(713); - /// The specified registry key is referenced by a predefined handle. - pub const PREDEFINED_HANDLE: Win32Error = Win32Error(714); - /// {Page Unlocked} The page protection of a locked page was changed to 'No Access' and the page was unlocked from memory and from the process. - pub const WAS_UNLOCKED: Win32Error = Win32Error(715); - /// %hs - pub const SERVICE_NOTIFICATION: Win32Error = Win32Error(716); - /// {Page Locked} One of the pages to lock was already locked. - pub const WAS_LOCKED: Win32Error = Win32Error(717); - /// Application popup: %1 : %2 - pub const LOG_HARD_ERROR: Win32Error = Win32Error(718); - /// ERROR_ALREADY_WIN32 - pub const ALREADY_WIN32: Win32Error = Win32Error(719); - /// {Machine Type Mismatch} The image file %hs is valid, but is for a machine type other than the current machine. - pub const IMAGE_MACHINE_TYPE_MISMATCH_EXE: Win32Error = Win32Error(720); - /// A yield execution was performed and no thread was available to run. - pub const NO_YIELD_PERFORMED: Win32Error = Win32Error(721); - /// The resumable flag to a timer API was ignored. - pub const TIMER_RESUME_IGNORED: Win32Error = Win32Error(722); - /// The arbiter has deferred arbitration of these resources to its parent. - pub const ARBITRATION_UNHANDLED: Win32Error = Win32Error(723); - /// The inserted CardBus device cannot be started because of a configuration error on "%hs". - pub const CARDBUS_NOT_SUPPORTED: Win32Error = Win32Error(724); - /// The CPUs in this multiprocessor system are not all the same revision level. - /// To use all processors the operating system restricts itself to the features of the least capable processor in the system. - /// Should problems occur with this system, contact the CPU manufacturer to see if this mix of processors is supported. - pub const MP_PROCESSOR_MISMATCH: Win32Error = Win32Error(725); - /// The system was put into hibernation. - pub const HIBERNATED: Win32Error = Win32Error(726); - /// The system was resumed from hibernation. - pub const RESUME_HIBERNATION: Win32Error = Win32Error(727); - /// Windows has detected that the system firmware (BIOS) was updated [previous firmware date = %2, current firmware date %3]. - pub const FIRMWARE_UPDATED: Win32Error = Win32Error(728); - /// A device driver is leaking locked I/O pages causing system degradation. - /// The system has automatically enabled tracking code in order to try and catch the culprit. - pub const DRIVERS_LEAKING_LOCKED_PAGES: Win32Error = Win32Error(729); - /// The system has awoken. - pub const WAKE_SYSTEM: Win32Error = Win32Error(730); - /// ERROR_WAIT_1 - pub const WAIT_1: Win32Error = Win32Error(731); - /// ERROR_WAIT_2 - pub const WAIT_2: Win32Error = Win32Error(732); - /// ERROR_WAIT_3 - pub const WAIT_3: Win32Error = Win32Error(733); - /// ERROR_WAIT_63 - pub const WAIT_63: Win32Error = Win32Error(734); - /// ERROR_ABANDONED_WAIT_0 - pub const ABANDONED_WAIT_0: Win32Error = Win32Error(735); - /// ERROR_ABANDONED_WAIT_63 - pub const ABANDONED_WAIT_63: Win32Error = Win32Error(736); - /// ERROR_USER_APC - pub const USER_APC: Win32Error = Win32Error(737); - /// ERROR_KERNEL_APC - pub const KERNEL_APC: Win32Error = Win32Error(738); - /// ERROR_ALERTED - pub const ALERTED: Win32Error = Win32Error(739); - /// The requested operation requires elevation. - pub const ELEVATION_REQUIRED: Win32Error = Win32Error(740); - /// A reparse should be performed by the Object Manager since the name of the file resulted in a symbolic link. - pub const REPARSE: Win32Error = Win32Error(741); - /// An open/create operation completed while an oplock break is underway. - pub const OPLOCK_BREAK_IN_PROGRESS: Win32Error = Win32Error(742); - /// A new volume has been mounted by a file system. - pub const VOLUME_MOUNTED: Win32Error = Win32Error(743); - /// This success level status indicates that the transaction state already exists for the registry sub-tree, but that a transaction commit was previously aborted. The commit has now been completed. - pub const RXACT_COMMITTED: Win32Error = Win32Error(744); - /// This indicates that a notify change request has been completed due to closing the handle which made the notify change request. - pub const NOTIFY_CLEANUP: Win32Error = Win32Error(745); - /// {Connect Failure on Primary Transport} An attempt was made to connect to the remote server %hs on the primary transport, but the connection failed. - /// The computer WAS able to connect on a secondary transport. - pub const PRIMARY_TRANSPORT_CONNECT_FAILED: Win32Error = Win32Error(746); - /// Page fault was a transition fault. - pub const PAGE_FAULT_TRANSITION: Win32Error = Win32Error(747); - /// Page fault was a demand zero fault. - pub const PAGE_FAULT_DEMAND_ZERO: Win32Error = Win32Error(748); - /// Page fault was a demand zero fault. - pub const PAGE_FAULT_COPY_ON_WRITE: Win32Error = Win32Error(749); - /// Page fault was a demand zero fault. - pub const PAGE_FAULT_GUARD_PAGE: Win32Error = Win32Error(750); - /// Page fault was satisfied by reading from a secondary storage device. - pub const PAGE_FAULT_PAGING_FILE: Win32Error = Win32Error(751); - /// Cached page was locked during operation. - pub const CACHE_PAGE_LOCKED: Win32Error = Win32Error(752); - /// Crash dump exists in paging file. - pub const CRASH_DUMP: Win32Error = Win32Error(753); - /// Specified buffer contains all zeros. - pub const BUFFER_ALL_ZEROS: Win32Error = Win32Error(754); - /// A reparse should be performed by the Object Manager since the name of the file resulted in a symbolic link. - pub const REPARSE_OBJECT: Win32Error = Win32Error(755); - /// The device has succeeded a query-stop and its resource requirements have changed. - pub const RESOURCE_REQUIREMENTS_CHANGED: Win32Error = Win32Error(756); - /// The translator has translated these resources into the global space and no further translations should be performed. - pub const TRANSLATION_COMPLETE: Win32Error = Win32Error(757); - /// A process being terminated has no threads to terminate. - pub const NOTHING_TO_TERMINATE: Win32Error = Win32Error(758); - /// The specified process is not part of a job. - pub const PROCESS_NOT_IN_JOB: Win32Error = Win32Error(759); - /// The specified process is part of a job. - pub const PROCESS_IN_JOB: Win32Error = Win32Error(760); - /// {Volume Shadow Copy Service} The system is now ready for hibernation. - pub const VOLSNAP_HIBERNATE_READY: Win32Error = Win32Error(761); - /// A file system or file system filter driver has successfully completed an FsFilter operation. - pub const FSFILTER_OP_COMPLETED_SUCCESSFULLY: Win32Error = Win32Error(762); - /// The specified interrupt vector was already connected. - pub const INTERRUPT_VECTOR_ALREADY_CONNECTED: Win32Error = Win32Error(763); - /// The specified interrupt vector is still connected. - pub const INTERRUPT_STILL_CONNECTED: Win32Error = Win32Error(764); - /// An operation is blocked waiting for an oplock. - pub const WAIT_FOR_OPLOCK: Win32Error = Win32Error(765); - /// Debugger handled exception. - pub const DBG_EXCEPTION_HANDLED: Win32Error = Win32Error(766); - /// Debugger continued. - pub const DBG_CONTINUE: Win32Error = Win32Error(767); - /// An exception occurred in a user mode callback and the kernel callback frame should be removed. - pub const CALLBACK_POP_STACK: Win32Error = Win32Error(768); - /// Compression is disabled for this volume. - pub const COMPRESSION_DISABLED: Win32Error = Win32Error(769); - /// The data provider cannot fetch backwards through a result set. - pub const CANTFETCHBACKWARDS: Win32Error = Win32Error(770); - /// The data provider cannot scroll backwards through a result set. - pub const CANTSCROLLBACKWARDS: Win32Error = Win32Error(771); - /// The data provider requires that previously fetched data is released before asking for more data. - pub const ROWSNOTRELEASED: Win32Error = Win32Error(772); - /// The data provider was not able to interpret the flags set for a column binding in an accessor. - pub const BAD_ACCESSOR_FLAGS: Win32Error = Win32Error(773); - /// One or more errors occurred while processing the request. - pub const ERRORS_ENCOUNTERED: Win32Error = Win32Error(774); - /// The implementation is not capable of performing the request. - pub const NOT_CAPABLE: Win32Error = Win32Error(775); - /// The client of a component requested an operation which is not valid given the state of the component instance. - pub const REQUEST_OUT_OF_SEQUENCE: Win32Error = Win32Error(776); - /// A version number could not be parsed. - pub const VERSION_PARSE_ERROR: Win32Error = Win32Error(777); - /// The iterator's start position is invalid. - pub const BADSTARTPOSITION: Win32Error = Win32Error(778); - /// The hardware has reported an uncorrectable memory error. - pub const MEMORY_HARDWARE: Win32Error = Win32Error(779); - /// The attempted operation required self healing to be enabled. - pub const DISK_REPAIR_DISABLED: Win32Error = Win32Error(780); - /// The Desktop heap encountered an error while allocating session memory. - /// There is more information in the system event log. - pub const INSUFFICIENT_RESOURCE_FOR_SPECIFIED_SHARED_SECTION_SIZE: Win32Error = - Win32Error(781); - /// The system power state is transitioning from %2 to %3. - pub const SYSTEM_POWERSTATE_TRANSITION: Win32Error = Win32Error(782); - /// The system power state is transitioning from %2 to %3 but could enter %4. - pub const SYSTEM_POWERSTATE_COMPLEX_TRANSITION: Win32Error = Win32Error(783); - /// A thread is getting dispatched with MCA EXCEPTION because of MCA. - pub const MCA_EXCEPTION: Win32Error = Win32Error(784); - /// Access to %1 is monitored by policy rule %2. - pub const ACCESS_AUDIT_BY_POLICY: Win32Error = Win32Error(785); - /// Access to %1 has been restricted by your Administrator by policy rule %2. - pub const ACCESS_DISABLED_NO_SAFER_UI_BY_POLICY: Win32Error = Win32Error(786); - /// A valid hibernation file has been invalidated and should be abandoned. - pub const ABANDON_HIBERFILE: Win32Error = Win32Error(787); - /// {Delayed Write Failed} Windows was unable to save all the data for the file %hs; the data has been lost. - /// This error may be caused by network connectivity issues. Please try to save this file elsewhere. - pub const LOST_WRITEBEHIND_DATA_NETWORK_DISCONNECTED: Win32Error = Win32Error(788); - /// {Delayed Write Failed} Windows was unable to save all the data for the file %hs; the data has been lost. - /// This error was returned by the server on which the file exists. Please try to save this file elsewhere. - pub const LOST_WRITEBEHIND_DATA_NETWORK_SERVER_ERROR: Win32Error = Win32Error(789); - /// {Delayed Write Failed} Windows was unable to save all the data for the file %hs; the data has been lost. - /// This error may be caused if the device has been removed or the media is write-protected. - pub const LOST_WRITEBEHIND_DATA_LOCAL_DISK_ERROR: Win32Error = Win32Error(790); - /// The resources required for this device conflict with the MCFG table. - pub const BAD_MCFG_TABLE: Win32Error = Win32Error(791); - /// The volume repair could not be performed while it is online. - /// Please schedule to take the volume offline so that it can be repaired. - pub const DISK_REPAIR_REDIRECTED: Win32Error = Win32Error(792); - /// The volume repair was not successful. - pub const DISK_REPAIR_UNSUCCESSFUL: Win32Error = Win32Error(793); - /// One of the volume corruption logs is full. - /// Further corruptions that may be detected won't be logged. - pub const CORRUPT_LOG_OVERFULL: Win32Error = Win32Error(794); - /// One of the volume corruption logs is internally corrupted and needs to be recreated. - /// The volume may contain undetected corruptions and must be scanned. - pub const CORRUPT_LOG_CORRUPTED: Win32Error = Win32Error(795); - /// One of the volume corruption logs is unavailable for being operated on. - pub const CORRUPT_LOG_UNAVAILABLE: Win32Error = Win32Error(796); - /// One of the volume corruption logs was deleted while still having corruption records in them. - /// The volume contains detected corruptions and must be scanned. - pub const CORRUPT_LOG_DELETED_FULL: Win32Error = Win32Error(797); - /// One of the volume corruption logs was cleared by chkdsk and no longer contains real corruptions. - pub const CORRUPT_LOG_CLEARED: Win32Error = Win32Error(798); - /// Orphaned files exist on the volume but could not be recovered because no more new names could be created in the recovery directory. Files must be moved from the recovery directory. - pub const ORPHAN_NAME_EXHAUSTED: Win32Error = Win32Error(799); - /// The oplock that was associated with this handle is now associated with a different handle. - pub const OPLOCK_SWITCHED_TO_NEW_HANDLE: Win32Error = Win32Error(800); - /// An oplock of the requested level cannot be granted. An oplock of a lower level may be available. - pub const CANNOT_GRANT_REQUESTED_OPLOCK: Win32Error = Win32Error(801); - /// The operation did not complete successfully because it would cause an oplock to be broken. - /// The caller has requested that existing oplocks not be broken. - pub const CANNOT_BREAK_OPLOCK: Win32Error = Win32Error(802); - /// The handle with which this oplock was associated has been closed. The oplock is now broken. - pub const OPLOCK_HANDLE_CLOSED: Win32Error = Win32Error(803); - /// The specified access control entry (ACE) does not contain a condition. - pub const NO_ACE_CONDITION: Win32Error = Win32Error(804); - /// The specified access control entry (ACE) contains an invalid condition. - pub const INVALID_ACE_CONDITION: Win32Error = Win32Error(805); - /// Access to the specified file handle has been revoked. - pub const FILE_HANDLE_REVOKED: Win32Error = Win32Error(806); - /// An image file was mapped at a different address from the one specified in the image file but fixups will still be automatically performed on the image. - pub const IMAGE_AT_DIFFERENT_BASE: Win32Error = Win32Error(807); - /// Access to the extended attribute was denied. - pub const EA_ACCESS_DENIED: Win32Error = Win32Error(994); - /// The I/O operation has been aborted because of either a thread exit or an application request. - pub const OPERATION_ABORTED: Win32Error = Win32Error(995); - /// Overlapped I/O event is not in a signaled state. - pub const IO_INCOMPLETE: Win32Error = Win32Error(996); - /// Overlapped I/O operation is in progress. - pub const IO_PENDING: Win32Error = Win32Error(997); - /// Invalid access to memory location. - pub const NOACCESS: Win32Error = Win32Error(998); - /// Error performing inpage operation. - pub const SWAPERROR: Win32Error = Win32Error(999); - /// Recursion too deep; the stack overflowed. - pub const STACK_OVERFLOW: Win32Error = Win32Error(1001); - /// The window cannot act on the sent message. - pub const INVALID_MESSAGE: Win32Error = Win32Error(1002); - /// Cannot complete this function. - pub const CAN_NOT_COMPLETE: Win32Error = Win32Error(1003); - /// Invalid flags. - pub const INVALID_FLAGS: Win32Error = Win32Error(1004); - /// The volume does not contain a recognized file system. - /// Please make sure that all required file system drivers are loaded and that the volume is not corrupted. - pub const UNRECOGNIZED_VOLUME: Win32Error = Win32Error(1005); - /// The volume for a file has been externally altered so that the opened file is no longer valid. - pub const FILE_INVALID: Win32Error = Win32Error(1006); - /// The requested operation cannot be performed in full-screen mode. - pub const FULLSCREEN_MODE: Win32Error = Win32Error(1007); - /// An attempt was made to reference a token that does not exist. - pub const NO_TOKEN: Win32Error = Win32Error(1008); - /// The configuration registry database is corrupt. - pub const BADDB: Win32Error = Win32Error(1009); - /// The configuration registry key is invalid. - pub const BADKEY: Win32Error = Win32Error(1010); - /// The configuration registry key could not be opened. - pub const CANTOPEN: Win32Error = Win32Error(1011); - /// The configuration registry key could not be read. - pub const CANTREAD: Win32Error = Win32Error(1012); - /// The configuration registry key could not be written. - pub const CANTWRITE: Win32Error = Win32Error(1013); - /// One of the files in the registry database had to be recovered by use of a log or alternate copy. The recovery was successful. - pub const REGISTRY_RECOVERED: Win32Error = Win32Error(1014); - /// The registry is corrupted. The structure of one of the files containing registry data is corrupted, or the system's memory image of the file is corrupted, or the file could not be recovered because the alternate copy or log was absent or corrupted. - pub const REGISTRY_CORRUPT: Win32Error = Win32Error(1015); - /// An I/O operation initiated by the registry failed unrecoverably. - /// The registry could not read in, or write out, or flush, one of the files that contain the system's image of the registry. - pub const REGISTRY_IO_FAILED: Win32Error = Win32Error(1016); - /// The system has attempted to load or restore a file into the registry, but the specified file is not in a registry file format. - pub const NOT_REGISTRY_FILE: Win32Error = Win32Error(1017); - /// Illegal operation attempted on a registry key that has been marked for deletion. - pub const KEY_DELETED: Win32Error = Win32Error(1018); - /// System could not allocate the required space in a registry log. - pub const NO_LOG_SPACE: Win32Error = Win32Error(1019); - /// Cannot create a symbolic link in a registry key that already has subkeys or values. - pub const KEY_HAS_CHILDREN: Win32Error = Win32Error(1020); - /// Cannot create a stable subkey under a volatile parent key. - pub const CHILD_MUST_BE_VOLATILE: Win32Error = Win32Error(1021); - /// A notify change request is being completed and the information is not being returned in the caller's buffer. - /// The caller now needs to enumerate the files to find the changes. - pub const NOTIFY_ENUM_DIR: Win32Error = Win32Error(1022); - /// A stop control has been sent to a service that other running services are dependent on. - pub const DEPENDENT_SERVICES_RUNNING: Win32Error = Win32Error(1051); - /// The requested control is not valid for this service. - pub const INVALID_SERVICE_CONTROL: Win32Error = Win32Error(1052); - /// The service did not respond to the start or control request in a timely fashion. - pub const SERVICE_REQUEST_TIMEOUT: Win32Error = Win32Error(1053); - /// A thread could not be created for the service. - pub const SERVICE_NO_THREAD: Win32Error = Win32Error(1054); - /// The service database is locked. - pub const SERVICE_DATABASE_LOCKED: Win32Error = Win32Error(1055); - /// An instance of the service is already running. - pub const SERVICE_ALREADY_RUNNING: Win32Error = Win32Error(1056); - /// The account name is invalid or does not exist, or the password is invalid for the account name specified. - pub const INVALID_SERVICE_ACCOUNT: Win32Error = Win32Error(1057); - /// The service cannot be started, either because it is disabled or because it has no enabled devices associated with it. - pub const SERVICE_DISABLED: Win32Error = Win32Error(1058); - /// Circular service dependency was specified. - pub const CIRCULAR_DEPENDENCY: Win32Error = Win32Error(1059); - /// The specified service does not exist as an installed service. - pub const SERVICE_DOES_NOT_EXIST: Win32Error = Win32Error(1060); - /// The service cannot accept control messages at this time. - pub const SERVICE_CANNOT_ACCEPT_CTRL: Win32Error = Win32Error(1061); - /// The service has not been started. - pub const SERVICE_NOT_ACTIVE: Win32Error = Win32Error(1062); - /// The service process could not connect to the service controller. - pub const FAILED_SERVICE_CONTROLLER_CONNECT: Win32Error = Win32Error(1063); - /// An exception occurred in the service when handling the control request. - pub const EXCEPTION_IN_SERVICE: Win32Error = Win32Error(1064); - /// The database specified does not exist. - pub const DATABASE_DOES_NOT_EXIST: Win32Error = Win32Error(1065); - /// The service has returned a service-specific error code. - pub const SERVICE_SPECIFIC_ERROR: Win32Error = Win32Error(1066); - /// The process terminated unexpectedly. - pub const PROCESS_ABORTED: Win32Error = Win32Error(1067); - /// The dependency service or group failed to start. - pub const SERVICE_DEPENDENCY_FAIL: Win32Error = Win32Error(1068); - /// The service did not start due to a logon failure. - pub const SERVICE_LOGON_FAILED: Win32Error = Win32Error(1069); - /// After starting, the service hung in a start-pending state. - pub const SERVICE_START_HANG: Win32Error = Win32Error(1070); - /// The specified service database lock is invalid. - pub const INVALID_SERVICE_LOCK: Win32Error = Win32Error(1071); - /// The specified service has been marked for deletion. - pub const SERVICE_MARKED_FOR_DELETE: Win32Error = Win32Error(1072); - /// The specified service already exists. - pub const SERVICE_EXISTS: Win32Error = Win32Error(1073); - /// The system is currently running with the last-known-good configuration. - pub const ALREADY_RUNNING_LKG: Win32Error = Win32Error(1074); - /// The dependency service does not exist or has been marked for deletion. - pub const SERVICE_DEPENDENCY_DELETED: Win32Error = Win32Error(1075); - /// The current boot has already been accepted for use as the last-known-good control set. - pub const BOOT_ALREADY_ACCEPTED: Win32Error = Win32Error(1076); - /// No attempts to start the service have been made since the last boot. - pub const SERVICE_NEVER_STARTED: Win32Error = Win32Error(1077); - /// The name is already in use as either a service name or a service display name. - pub const DUPLICATE_SERVICE_NAME: Win32Error = Win32Error(1078); - /// The account specified for this service is different from the account specified for other services running in the same process. - pub const DIFFERENT_SERVICE_ACCOUNT: Win32Error = Win32Error(1079); - /// Failure actions can only be set for Win32 services, not for drivers. - pub const CANNOT_DETECT_DRIVER_FAILURE: Win32Error = Win32Error(1080); - /// This service runs in the same process as the service control manager. - /// Therefore, the service control manager cannot take action if this service's process terminates unexpectedly. - pub const CANNOT_DETECT_PROCESS_ABORT: Win32Error = Win32Error(1081); - /// No recovery program has been configured for this service. - pub const NO_RECOVERY_PROGRAM: Win32Error = Win32Error(1082); - /// The executable program that this service is configured to run in does not implement the service. - pub const SERVICE_NOT_IN_EXE: Win32Error = Win32Error(1083); - /// This service cannot be started in Safe Mode. - pub const NOT_SAFEBOOT_SERVICE: Win32Error = Win32Error(1084); - /// The physical end of the tape has been reached. - pub const END_OF_MEDIA: Win32Error = Win32Error(1100); - /// A tape access reached a filemark. - pub const FILEMARK_DETECTED: Win32Error = Win32Error(1101); - /// The beginning of the tape or a partition was encountered. - pub const BEGINNING_OF_MEDIA: Win32Error = Win32Error(1102); - /// A tape access reached the end of a set of files. - pub const SETMARK_DETECTED: Win32Error = Win32Error(1103); - /// No more data is on the tape. - pub const NO_DATA_DETECTED: Win32Error = Win32Error(1104); - /// Tape could not be partitioned. - pub const PARTITION_FAILURE: Win32Error = Win32Error(1105); - /// When accessing a new tape of a multivolume partition, the current block size is incorrect. - pub const INVALID_BLOCK_LENGTH: Win32Error = Win32Error(1106); - /// Tape partition information could not be found when loading a tape. - pub const DEVICE_NOT_PARTITIONED: Win32Error = Win32Error(1107); - /// Unable to lock the media eject mechanism. - pub const UNABLE_TO_LOCK_MEDIA: Win32Error = Win32Error(1108); - /// Unable to unload the media. - pub const UNABLE_TO_UNLOAD_MEDIA: Win32Error = Win32Error(1109); - /// The media in the drive may have changed. - pub const MEDIA_CHANGED: Win32Error = Win32Error(1110); - /// The I/O bus was reset. - pub const BUS_RESET: Win32Error = Win32Error(1111); - /// No media in drive. - pub const NO_MEDIA_IN_DRIVE: Win32Error = Win32Error(1112); - /// No mapping for the Unicode character exists in the target multi-byte code page. - pub const NO_UNICODE_TRANSLATION: Win32Error = Win32Error(1113); - /// A dynamic link library (DLL) initialization routine failed. - pub const DLL_INIT_FAILED: Win32Error = Win32Error(1114); - /// A system shutdown is in progress. - pub const SHUTDOWN_IN_PROGRESS: Win32Error = Win32Error(1115); - /// Unable to abort the system shutdown because no shutdown was in progress. - pub const NO_SHUTDOWN_IN_PROGRESS: Win32Error = Win32Error(1116); - /// The request could not be performed because of an I/O device error. - pub const IO_DEVICE: Win32Error = Win32Error(1117); - /// No serial device was successfully initialized. The serial driver will unload. - pub const SERIAL_NO_DEVICE: Win32Error = Win32Error(1118); - /// Unable to open a device that was sharing an interrupt request (IRQ) with other devices. - /// At least one other device that uses that IRQ was already opened. - pub const IRQ_BUSY: Win32Error = Win32Error(1119); - /// A serial I/O operation was completed by another write to the serial port. The IOCTL_SERIAL_XOFF_COUNTER reached zero.) - pub const MORE_WRITES: Win32Error = Win32Error(1120); - /// A serial I/O operation completed because the timeout period expired. - /// The IOCTL_SERIAL_XOFF_COUNTER did not reach zero.) - pub const COUNTER_TIMEOUT: Win32Error = Win32Error(1121); - /// No ID address mark was found on the floppy disk. - pub const FLOPPY_ID_MARK_NOT_FOUND: Win32Error = Win32Error(1122); - /// Mismatch between the floppy disk sector ID field and the floppy disk controller track address. - pub const FLOPPY_WRONG_CYLINDER: Win32Error = Win32Error(1123); - /// The floppy disk controller reported an error that is not recognized by the floppy disk driver. - pub const FLOPPY_UNKNOWN_ERROR: Win32Error = Win32Error(1124); - /// The floppy disk controller returned inconsistent results in its registers. - pub const FLOPPY_BAD_REGISTERS: Win32Error = Win32Error(1125); - /// While accessing the hard disk, a recalibrate operation failed, even after retries. - pub const DISK_RECALIBRATE_FAILED: Win32Error = Win32Error(1126); - /// While accessing the hard disk, a disk operation failed even after retries. - pub const DISK_OPERATION_FAILED: Win32Error = Win32Error(1127); - /// While accessing the hard disk, a disk controller reset was needed, but even that failed. - pub const DISK_RESET_FAILED: Win32Error = Win32Error(1128); - /// Physical end of tape encountered. - pub const EOM_OVERFLOW: Win32Error = Win32Error(1129); - /// Not enough server storage is available to process this command. - pub const NOT_ENOUGH_SERVER_MEMORY: Win32Error = Win32Error(1130); - /// A potential deadlock condition has been detected. - pub const POSSIBLE_DEADLOCK: Win32Error = Win32Error(1131); - /// The base address or the file offset specified does not have the proper alignment. - pub const MAPPED_ALIGNMENT: Win32Error = Win32Error(1132); - /// An attempt to change the system power state was vetoed by another application or driver. - pub const SET_POWER_STATE_VETOED: Win32Error = Win32Error(1140); - /// The system BIOS failed an attempt to change the system power state. - pub const SET_POWER_STATE_FAILED: Win32Error = Win32Error(1141); - /// An attempt was made to create more links on a file than the file system supports. - pub const TOO_MANY_LINKS: Win32Error = Win32Error(1142); - /// The specified program requires a newer version of Windows. - pub const OLD_WIN_VERSION: Win32Error = Win32Error(1150); - /// The specified program is not a Windows or MS-DOS program. - pub const APP_WRONG_OS: Win32Error = Win32Error(1151); - /// Cannot start more than one instance of the specified program. - pub const SINGLE_INSTANCE_APP: Win32Error = Win32Error(1152); - /// The specified program was written for an earlier version of Windows. - pub const RMODE_APP: Win32Error = Win32Error(1153); - /// One of the library files needed to run this application is damaged. - pub const INVALID_DLL: Win32Error = Win32Error(1154); - /// No application is associated with the specified file for this operation. - pub const NO_ASSOCIATION: Win32Error = Win32Error(1155); - /// An error occurred in sending the command to the application. - pub const DDE_FAIL: Win32Error = Win32Error(1156); - /// One of the library files needed to run this application cannot be found. - pub const DLL_NOT_FOUND: Win32Error = Win32Error(1157); - /// The current process has used all of its system allowance of handles for Window Manager objects. - pub const NO_MORE_USER_HANDLES: Win32Error = Win32Error(1158); - /// The message can be used only with synchronous operations. - pub const MESSAGE_SYNC_ONLY: Win32Error = Win32Error(1159); - /// The indicated source element has no media. - pub const SOURCE_ELEMENT_EMPTY: Win32Error = Win32Error(1160); - /// The indicated destination element already contains media. - pub const DESTINATION_ELEMENT_FULL: Win32Error = Win32Error(1161); - /// The indicated element does not exist. - pub const ILLEGAL_ELEMENT_ADDRESS: Win32Error = Win32Error(1162); - /// The indicated element is part of a magazine that is not present. - pub const MAGAZINE_NOT_PRESENT: Win32Error = Win32Error(1163); - /// The indicated device requires reinitialization due to hardware errors. - pub const DEVICE_REINITIALIZATION_NEEDED: Win32Error = Win32Error(1164); - /// The device has indicated that cleaning is required before further operations are attempted. - pub const DEVICE_REQUIRES_CLEANING: Win32Error = Win32Error(1165); - /// The device has indicated that its door is open. - pub const DEVICE_DOOR_OPEN: Win32Error = Win32Error(1166); - /// The device is not connected. - pub const DEVICE_NOT_CONNECTED: Win32Error = Win32Error(1167); - /// Element not found. - pub const NOT_FOUND: Win32Error = Win32Error(1168); - /// There was no match for the specified key in the index. - pub const NO_MATCH: Win32Error = Win32Error(1169); - /// The property set specified does not exist on the object. - pub const SET_NOT_FOUND: Win32Error = Win32Error(1170); - /// The point passed to GetMouseMovePoints is not in the buffer. - pub const POINT_NOT_FOUND: Win32Error = Win32Error(1171); - /// The tracking (workstation) service is not running. - pub const NO_TRACKING_SERVICE: Win32Error = Win32Error(1172); - /// The Volume ID could not be found. - pub const NO_VOLUME_ID: Win32Error = Win32Error(1173); - /// Unable to remove the file to be replaced. - pub const UNABLE_TO_REMOVE_REPLACED: Win32Error = Win32Error(1175); - /// Unable to move the replacement file to the file to be replaced. - /// The file to be replaced has retained its original name. - pub const UNABLE_TO_MOVE_REPLACEMENT: Win32Error = Win32Error(1176); - /// Unable to move the replacement file to the file to be replaced. - /// The file to be replaced has been renamed using the backup name. - pub const UNABLE_TO_MOVE_REPLACEMENT_2: Win32Error = Win32Error(1177); - /// The volume change journal is being deleted. - pub const JOURNAL_DELETE_IN_PROGRESS: Win32Error = Win32Error(1178); - /// The volume change journal is not active. - pub const JOURNAL_NOT_ACTIVE: Win32Error = Win32Error(1179); - /// A file was found, but it may not be the correct file. - pub const POTENTIAL_FILE_FOUND: Win32Error = Win32Error(1180); - /// The journal entry has been deleted from the journal. - pub const JOURNAL_ENTRY_DELETED: Win32Error = Win32Error(1181); - /// A system shutdown has already been scheduled. - pub const SHUTDOWN_IS_SCHEDULED: Win32Error = Win32Error(1190); - /// The system shutdown cannot be initiated because there are other users logged on to the computer. - pub const SHUTDOWN_USERS_LOGGED_ON: Win32Error = Win32Error(1191); - /// The specified device name is invalid. - pub const BAD_DEVICE: Win32Error = Win32Error(1200); - /// The device is not currently connected but it is a remembered connection. - pub const CONNECTION_UNAVAIL: Win32Error = Win32Error(1201); - /// The local device name has a remembered connection to another network resource. - pub const DEVICE_ALREADY_REMEMBERED: Win32Error = Win32Error(1202); - /// The network path was either typed incorrectly, does not exist, or the network provider is not currently available. - /// Please try retyping the path or contact your network administrator. - pub const NO_NET_OR_BAD_PATH: Win32Error = Win32Error(1203); - /// The specified network provider name is invalid. - pub const BAD_PROVIDER: Win32Error = Win32Error(1204); - /// Unable to open the network connection profile. - pub const CANNOT_OPEN_PROFILE: Win32Error = Win32Error(1205); - /// The network connection profile is corrupted. - pub const BAD_PROFILE: Win32Error = Win32Error(1206); - /// Cannot enumerate a noncontainer. - pub const NOT_CONTAINER: Win32Error = Win32Error(1207); - /// An extended error has occurred. - pub const EXTENDED_ERROR: Win32Error = Win32Error(1208); - /// The format of the specified group name is invalid. - pub const INVALID_GROUPNAME: Win32Error = Win32Error(1209); - /// The format of the specified computer name is invalid. - pub const INVALID_COMPUTERNAME: Win32Error = Win32Error(1210); - /// The format of the specified event name is invalid. - pub const INVALID_EVENTNAME: Win32Error = Win32Error(1211); - /// The format of the specified domain name is invalid. - pub const INVALID_DOMAINNAME: Win32Error = Win32Error(1212); - /// The format of the specified service name is invalid. - pub const INVALID_SERVICENAME: Win32Error = Win32Error(1213); - /// The format of the specified network name is invalid. - pub const INVALID_NETNAME: Win32Error = Win32Error(1214); - /// The format of the specified share name is invalid. - pub const INVALID_SHARENAME: Win32Error = Win32Error(1215); - /// The format of the specified password is invalid. - pub const INVALID_PASSWORDNAME: Win32Error = Win32Error(1216); - /// The format of the specified message name is invalid. - pub const INVALID_MESSAGENAME: Win32Error = Win32Error(1217); - /// The format of the specified message destination is invalid. - pub const INVALID_MESSAGEDEST: Win32Error = Win32Error(1218); - /// Multiple connections to a server or shared resource by the same user, using more than one user name, are not allowed. - /// Disconnect all previous connections to the server or shared resource and try again. - pub const SESSION_CREDENTIAL_CONFLICT: Win32Error = Win32Error(1219); - /// An attempt was made to establish a session to a network server, but there are already too many sessions established to that server. - pub const REMOTE_SESSION_LIMIT_EXCEEDED: Win32Error = Win32Error(1220); - /// The workgroup or domain name is already in use by another computer on the network. - pub const DUP_DOMAINNAME: Win32Error = Win32Error(1221); - /// The network is not present or not started. - pub const NO_NETWORK: Win32Error = Win32Error(1222); - /// The operation was canceled by the user. - pub const CANCELLED: Win32Error = Win32Error(1223); - /// The requested operation cannot be performed on a file with a user-mapped section open. - pub const USER_MAPPED_FILE: Win32Error = Win32Error(1224); - /// The remote computer refused the network connection. - pub const CONNECTION_REFUSED: Win32Error = Win32Error(1225); - /// The network connection was gracefully closed. - pub const GRACEFUL_DISCONNECT: Win32Error = Win32Error(1226); - /// The network transport endpoint already has an address associated with it. - pub const ADDRESS_ALREADY_ASSOCIATED: Win32Error = Win32Error(1227); - /// An address has not yet been associated with the network endpoint. - pub const ADDRESS_NOT_ASSOCIATED: Win32Error = Win32Error(1228); - /// An operation was attempted on a nonexistent network connection. - pub const CONNECTION_INVALID: Win32Error = Win32Error(1229); - /// An invalid operation was attempted on an active network connection. - pub const CONNECTION_ACTIVE: Win32Error = Win32Error(1230); - /// The network location cannot be reached. - /// For information about network troubleshooting, see Windows Help. - pub const NETWORK_UNREACHABLE: Win32Error = Win32Error(1231); - /// The network location cannot be reached. - /// For information about network troubleshooting, see Windows Help. - pub const HOST_UNREACHABLE: Win32Error = Win32Error(1232); - /// The network location cannot be reached. - /// For information about network troubleshooting, see Windows Help. - pub const PROTOCOL_UNREACHABLE: Win32Error = Win32Error(1233); - /// No service is operating at the destination network endpoint on the remote system. - pub const PORT_UNREACHABLE: Win32Error = Win32Error(1234); - /// The request was aborted. - pub const REQUEST_ABORTED: Win32Error = Win32Error(1235); - /// The network connection was aborted by the local system. - pub const CONNECTION_ABORTED: Win32Error = Win32Error(1236); - /// The operation could not be completed. A retry should be performed. - pub const RETRY: Win32Error = Win32Error(1237); - /// A connection to the server could not be made because the limit on the number of concurrent connections for this account has been reached. - pub const CONNECTION_COUNT_LIMIT: Win32Error = Win32Error(1238); - /// Attempting to log in during an unauthorized time of day for this account. - pub const LOGIN_TIME_RESTRICTION: Win32Error = Win32Error(1239); - /// The account is not authorized to log in from this station. - pub const LOGIN_WKSTA_RESTRICTION: Win32Error = Win32Error(1240); - /// The network address could not be used for the operation requested. - pub const INCORRECT_ADDRESS: Win32Error = Win32Error(1241); - /// The service is already registered. - pub const ALREADY_REGISTERED: Win32Error = Win32Error(1242); - /// The specified service does not exist. - pub const SERVICE_NOT_FOUND: Win32Error = Win32Error(1243); - /// The operation being requested was not performed because the user has not been authenticated. - pub const NOT_AUTHENTICATED: Win32Error = Win32Error(1244); - /// The operation being requested was not performed because the user has not logged on to the network. The specified service does not exist. - pub const NOT_LOGGED_ON: Win32Error = Win32Error(1245); - /// Continue with work in progress. - pub const CONTINUE: Win32Error = Win32Error(1246); - /// An attempt was made to perform an initialization operation when initialization has already been completed. - pub const ALREADY_INITIALIZED: Win32Error = Win32Error(1247); - /// No more local devices. - pub const NO_MORE_DEVICES: Win32Error = Win32Error(1248); - /// The specified site does not exist. - pub const NO_SUCH_SITE: Win32Error = Win32Error(1249); - /// A domain controller with the specified name already exists. - pub const DOMAIN_CONTROLLER_EXISTS: Win32Error = Win32Error(1250); - /// This operation is supported only when you are connected to the server. - pub const ONLY_IF_CONNECTED: Win32Error = Win32Error(1251); - /// The group policy framework should call the extension even if there are no changes. - pub const OVERRIDE_NOCHANGES: Win32Error = Win32Error(1252); - /// The specified user does not have a valid profile. - pub const BAD_USER_PROFILE: Win32Error = Win32Error(1253); - /// This operation is not supported on a computer running Windows Server 2003 for Small Business Server. - pub const NOT_SUPPORTED_ON_SBS: Win32Error = Win32Error(1254); - /// The server machine is shutting down. - pub const SERVER_SHUTDOWN_IN_PROGRESS: Win32Error = Win32Error(1255); - /// The remote system is not available. - /// For information about network troubleshooting, see Windows Help. - pub const HOST_DOWN: Win32Error = Win32Error(1256); - /// The security identifier provided is not from an account domain. - pub const NON_ACCOUNT_SID: Win32Error = Win32Error(1257); - /// The security identifier provided does not have a domain component. - pub const NON_DOMAIN_SID: Win32Error = Win32Error(1258); - /// AppHelp dialog canceled thus preventing the application from starting. - pub const APPHELP_BLOCK: Win32Error = Win32Error(1259); - /// This program is blocked by group policy. - /// For more information, contact your system administrator. - pub const ACCESS_DISABLED_BY_POLICY: Win32Error = Win32Error(1260); - /// A program attempt to use an invalid register value. - /// Normally caused by an uninitialized register. This error is Itanium specific. - pub const REG_NAT_CONSUMPTION: Win32Error = Win32Error(1261); - /// The share is currently offline or does not exist. - pub const CSCSHARE_OFFLINE: Win32Error = Win32Error(1262); - /// The Kerberos protocol encountered an error while validating the KDC certificate during smartcard logon. - /// There is more information in the system event log. - pub const PKINIT_FAILURE: Win32Error = Win32Error(1263); - /// The Kerberos protocol encountered an error while attempting to utilize the smartcard subsystem. - pub const SMARTCARD_SUBSYSTEM_FAILURE: Win32Error = Win32Error(1264); - /// The system cannot contact a domain controller to service the authentication request. Please try again later. - pub const DOWNGRADE_DETECTED: Win32Error = Win32Error(1265); - /// The machine is locked and cannot be shut down without the force option. - pub const MACHINE_LOCKED: Win32Error = Win32Error(1271); - /// An application-defined callback gave invalid data when called. - pub const CALLBACK_SUPPLIED_INVALID_DATA: Win32Error = Win32Error(1273); - /// The group policy framework should call the extension in the synchronous foreground policy refresh. - pub const SYNC_FOREGROUND_REFRESH_REQUIRED: Win32Error = Win32Error(1274); - /// This driver has been blocked from loading. - pub const DRIVER_BLOCKED: Win32Error = Win32Error(1275); - /// A dynamic link library (DLL) referenced a module that was neither a DLL nor the process's executable image. - pub const INVALID_IMPORT_OF_NON_DLL: Win32Error = Win32Error(1276); - /// Windows cannot open this program since it has been disabled. - pub const ACCESS_DISABLED_WEBBLADE: Win32Error = Win32Error(1277); - /// Windows cannot open this program because the license enforcement system has been tampered with or become corrupted. - pub const ACCESS_DISABLED_WEBBLADE_TAMPER: Win32Error = Win32Error(1278); - /// A transaction recover failed. - pub const RECOVERY_FAILURE: Win32Error = Win32Error(1279); - /// The current thread has already been converted to a fiber. - pub const ALREADY_FIBER: Win32Error = Win32Error(1280); - /// The current thread has already been converted from a fiber. - pub const ALREADY_THREAD: Win32Error = Win32Error(1281); - /// The system detected an overrun of a stack-based buffer in this application. - /// This overrun could potentially allow a malicious user to gain control of this application. - pub const STACK_BUFFER_OVERRUN: Win32Error = Win32Error(1282); - /// Data present in one of the parameters is more than the function can operate on. - pub const PARAMETER_QUOTA_EXCEEDED: Win32Error = Win32Error(1283); - /// An attempt to do an operation on a debug object failed because the object is in the process of being deleted. - pub const DEBUGGER_INACTIVE: Win32Error = Win32Error(1284); - /// An attempt to delay-load a .dll or get a function address in a delay-loaded .dll failed. - pub const DELAY_LOAD_FAILED: Win32Error = Win32Error(1285); - /// %1 is a 16-bit application. You do not have permissions to execute 16-bit applications. - /// Check your permissions with your system administrator. - pub const VDM_DISALLOWED: Win32Error = Win32Error(1286); - /// Insufficient information exists to identify the cause of failure. - pub const UNIDENTIFIED_ERROR: Win32Error = Win32Error(1287); - /// The parameter passed to a C runtime function is incorrect. - pub const INVALID_CRUNTIME_PARAMETER: Win32Error = Win32Error(1288); - /// The operation occurred beyond the valid data length of the file. - pub const BEYOND_VDL: Win32Error = Win32Error(1289); - /// The service start failed since one or more services in the same process have an incompatible service SID type setting. - /// A service with restricted service SID type can only coexist in the same process with other services with a restricted SID type. - /// If the service SID type for this service was just configured, the hosting process must be restarted in order to start this service. - /// On Windows Server 2003 and Windows XP, an unrestricted service cannot coexist in the same process with other services. - /// The service with the unrestricted service SID type must be moved to an owned process in order to start this service. - pub const INCOMPATIBLE_SERVICE_SID_TYPE: Win32Error = Win32Error(1290); - /// The process hosting the driver for this device has been terminated. - pub const DRIVER_PROCESS_TERMINATED: Win32Error = Win32Error(1291); - /// An operation attempted to exceed an implementation-defined limit. - pub const IMPLEMENTATION_LIMIT: Win32Error = Win32Error(1292); - /// Either the target process, or the target thread's containing process, is a protected process. - pub const PROCESS_IS_PROTECTED: Win32Error = Win32Error(1293); - /// The service notification client is lagging too far behind the current state of services in the machine. - pub const SERVICE_NOTIFY_CLIENT_LAGGING: Win32Error = Win32Error(1294); - /// The requested file operation failed because the storage quota was exceeded. - /// To free up disk space, move files to a different location or delete unnecessary files. - /// For more information, contact your system administrator. - pub const DISK_QUOTA_EXCEEDED: Win32Error = Win32Error(1295); - /// The requested file operation failed because the storage policy blocks that type of file. - /// For more information, contact your system administrator. - pub const CONTENT_BLOCKED: Win32Error = Win32Error(1296); - /// A privilege that the service requires to function properly does not exist in the service account configuration. - /// You may use the Services Microsoft Management Console (MMC) snap-in (services.msc) and the Local Security Settings MMC snap-in (secpol.msc) to view the service configuration and the account configuration. - pub const INCOMPATIBLE_SERVICE_PRIVILEGE: Win32Error = Win32Error(1297); - /// A thread involved in this operation appears to be unresponsive. - pub const APP_HANG: Win32Error = Win32Error(1298); - /// Indicates a particular Security ID may not be assigned as the label of an object. - pub const INVALID_LABEL: Win32Error = Win32Error(1299); - /// Not all privileges or groups referenced are assigned to the caller. - pub const NOT_ALL_ASSIGNED: Win32Error = Win32Error(1300); - /// Some mapping between account names and security IDs was not done. - pub const SOME_NOT_MAPPED: Win32Error = Win32Error(1301); - /// No system quota limits are specifically set for this account. - pub const NO_QUOTAS_FOR_ACCOUNT: Win32Error = Win32Error(1302); - /// No encryption key is available. A well-known encryption key was returned. - pub const LOCAL_USER_SESSION_KEY: Win32Error = Win32Error(1303); - /// The password is too complex to be converted to a LAN Manager password. - /// The LAN Manager password returned is a NULL string. - pub const NULL_LM_PASSWORD: Win32Error = Win32Error(1304); - /// The revision level is unknown. - pub const UNKNOWN_REVISION: Win32Error = Win32Error(1305); - /// Indicates two revision levels are incompatible. - pub const REVISION_MISMATCH: Win32Error = Win32Error(1306); - /// This security ID may not be assigned as the owner of this object. - pub const INVALID_OWNER: Win32Error = Win32Error(1307); - /// This security ID may not be assigned as the primary group of an object. - pub const INVALID_PRIMARY_GROUP: Win32Error = Win32Error(1308); - /// An attempt has been made to operate on an impersonation token by a thread that is not currently impersonating a client. - pub const NO_IMPERSONATION_TOKEN: Win32Error = Win32Error(1309); - /// The group may not be disabled. - pub const CANT_DISABLE_MANDATORY: Win32Error = Win32Error(1310); - /// There are currently no logon servers available to service the logon request. - pub const NO_LOGON_SERVERS: Win32Error = Win32Error(1311); - /// A specified logon session does not exist. It may already have been terminated. - pub const NO_SUCH_LOGON_SESSION: Win32Error = Win32Error(1312); - /// A specified privilege does not exist. - pub const NO_SUCH_PRIVILEGE: Win32Error = Win32Error(1313); - /// A required privilege is not held by the client. - pub const PRIVILEGE_NOT_HELD: Win32Error = Win32Error(1314); - /// The name provided is not a properly formed account name. - pub const INVALID_ACCOUNT_NAME: Win32Error = Win32Error(1315); - /// The specified account already exists. - pub const USER_EXISTS: Win32Error = Win32Error(1316); - /// The specified account does not exist. - pub const NO_SUCH_USER: Win32Error = Win32Error(1317); - /// The specified group already exists. - pub const GROUP_EXISTS: Win32Error = Win32Error(1318); - /// The specified group does not exist. - pub const NO_SUCH_GROUP: Win32Error = Win32Error(1319); - /// Either the specified user account is already a member of the specified group, or the specified group cannot be deleted because it contains a member. - pub const MEMBER_IN_GROUP: Win32Error = Win32Error(1320); - /// The specified user account is not a member of the specified group account. - pub const MEMBER_NOT_IN_GROUP: Win32Error = Win32Error(1321); - /// This operation is disallowed as it could result in an administration account being disabled, deleted or unable to log on. - pub const LAST_ADMIN: Win32Error = Win32Error(1322); - /// Unable to update the password. The value provided as the current password is incorrect. - pub const WRONG_PASSWORD: Win32Error = Win32Error(1323); - /// Unable to update the password. The value provided for the new password contains values that are not allowed in passwords. - pub const ILL_FORMED_PASSWORD: Win32Error = Win32Error(1324); - /// Unable to update the password. The value provided for the new password does not meet the length, complexity, or history requirements of the domain. - pub const PASSWORD_RESTRICTION: Win32Error = Win32Error(1325); - /// The user name or password is incorrect. - pub const LOGON_FAILURE: Win32Error = Win32Error(1326); - /// Account restrictions are preventing this user from signing in. - /// For example: blank passwords aren't allowed, sign-in times are limited, or a policy restriction has been enforced. - pub const ACCOUNT_RESTRICTION: Win32Error = Win32Error(1327); - /// Your account has time restrictions that keep you from signing in right now. - pub const INVALID_LOGON_HOURS: Win32Error = Win32Error(1328); - /// This user isn't allowed to sign in to this computer. - pub const INVALID_WORKSTATION: Win32Error = Win32Error(1329); - /// The password for this account has expired. - pub const PASSWORD_EXPIRED: Win32Error = Win32Error(1330); - /// This user can't sign in because this account is currently disabled. - pub const ACCOUNT_DISABLED: Win32Error = Win32Error(1331); - /// No mapping between account names and security IDs was done. - pub const NONE_MAPPED: Win32Error = Win32Error(1332); - /// Too many local user identifiers (LUIDs) were requested at one time. - pub const TOO_MANY_LUIDS_REQUESTED: Win32Error = Win32Error(1333); - /// No more local user identifiers (LUIDs) are available. - pub const LUIDS_EXHAUSTED: Win32Error = Win32Error(1334); - /// The subauthority part of a security ID is invalid for this particular use. - pub const INVALID_SUB_AUTHORITY: Win32Error = Win32Error(1335); - /// The access control list (ACL) structure is invalid. - pub const INVALID_ACL: Win32Error = Win32Error(1336); - /// The security ID structure is invalid. - pub const INVALID_SID: Win32Error = Win32Error(1337); - /// The security descriptor structure is invalid. - pub const INVALID_SECURITY_DESCR: Win32Error = Win32Error(1338); - /// The inherited access control list (ACL) or access control entry (ACE) could not be built. - pub const BAD_INHERITANCE_ACL: Win32Error = Win32Error(1340); - /// The server is currently disabled. - pub const SERVER_DISABLED: Win32Error = Win32Error(1341); - /// The server is currently enabled. - pub const SERVER_NOT_DISABLED: Win32Error = Win32Error(1342); - /// The value provided was an invalid value for an identifier authority. - pub const INVALID_ID_AUTHORITY: Win32Error = Win32Error(1343); - /// No more memory is available for security information updates. - pub const ALLOTTED_SPACE_EXCEEDED: Win32Error = Win32Error(1344); - /// The specified attributes are invalid, or incompatible with the attributes for the group as a whole. - pub const INVALID_GROUP_ATTRIBUTES: Win32Error = Win32Error(1345); - /// Either a required impersonation level was not provided, or the provided impersonation level is invalid. - pub const BAD_IMPERSONATION_LEVEL: Win32Error = Win32Error(1346); - /// Cannot open an anonymous level security token. - pub const CANT_OPEN_ANONYMOUS: Win32Error = Win32Error(1347); - /// The validation information class requested was invalid. - pub const BAD_VALIDATION_CLASS: Win32Error = Win32Error(1348); - /// The type of the token is inappropriate for its attempted use. - pub const BAD_TOKEN_TYPE: Win32Error = Win32Error(1349); - /// Unable to perform a security operation on an object that has no associated security. - pub const NO_SECURITY_ON_OBJECT: Win32Error = Win32Error(1350); - /// Configuration information could not be read from the domain controller, either because the machine is unavailable, or access has been denied. - pub const CANT_ACCESS_DOMAIN_INFO: Win32Error = Win32Error(1351); - /// The security account manager (SAM) or local security authority (LSA) server was in the wrong state to perform the security operation. - pub const INVALID_SERVER_STATE: Win32Error = Win32Error(1352); - /// The domain was in the wrong state to perform the security operation. - pub const INVALID_DOMAIN_STATE: Win32Error = Win32Error(1353); - /// This operation is only allowed for the Primary Domain Controller of the domain. - pub const INVALID_DOMAIN_ROLE: Win32Error = Win32Error(1354); - /// The specified domain either does not exist or could not be contacted. - pub const NO_SUCH_DOMAIN: Win32Error = Win32Error(1355); - /// The specified domain already exists. - pub const DOMAIN_EXISTS: Win32Error = Win32Error(1356); - /// An attempt was made to exceed the limit on the number of domains per server. - pub const DOMAIN_LIMIT_EXCEEDED: Win32Error = Win32Error(1357); - /// Unable to complete the requested operation because of either a catastrophic media failure or a data structure corruption on the disk. - pub const INTERNAL_DB_CORRUPTION: Win32Error = Win32Error(1358); - /// An internal error occurred. - pub const INTERNAL_ERROR: Win32Error = Win32Error(1359); - /// Generic access types were contained in an access mask which should already be mapped to nongeneric types. - pub const GENERIC_NOT_MAPPED: Win32Error = Win32Error(1360); - /// A security descriptor is not in the right format (absolute or self-relative). - pub const BAD_DESCRIPTOR_FORMAT: Win32Error = Win32Error(1361); - /// The requested action is restricted for use by logon processes only. - /// The calling process has not registered as a logon process. - pub const NOT_LOGON_PROCESS: Win32Error = Win32Error(1362); - /// Cannot start a new logon session with an ID that is already in use. - pub const LOGON_SESSION_EXISTS: Win32Error = Win32Error(1363); - /// A specified authentication package is unknown. - pub const NO_SUCH_PACKAGE: Win32Error = Win32Error(1364); - /// The logon session is not in a state that is consistent with the requested operation. - pub const BAD_LOGON_SESSION_STATE: Win32Error = Win32Error(1365); - /// The logon session ID is already in use. - pub const LOGON_SESSION_COLLISION: Win32Error = Win32Error(1366); - /// A logon request contained an invalid logon type value. - pub const INVALID_LOGON_TYPE: Win32Error = Win32Error(1367); - /// Unable to impersonate using a named pipe until data has been read from that pipe. - pub const CANNOT_IMPERSONATE: Win32Error = Win32Error(1368); - /// The transaction state of a registry subtree is incompatible with the requested operation. - pub const RXACT_INVALID_STATE: Win32Error = Win32Error(1369); - /// An internal security database corruption has been encountered. - pub const RXACT_COMMIT_FAILURE: Win32Error = Win32Error(1370); - /// Cannot perform this operation on built-in accounts. - pub const SPECIAL_ACCOUNT: Win32Error = Win32Error(1371); - /// Cannot perform this operation on this built-in special group. - pub const SPECIAL_GROUP: Win32Error = Win32Error(1372); - /// Cannot perform this operation on this built-in special user. - pub const SPECIAL_USER: Win32Error = Win32Error(1373); - /// The user cannot be removed from a group because the group is currently the user's primary group. - pub const MEMBERS_PRIMARY_GROUP: Win32Error = Win32Error(1374); - /// The token is already in use as a primary token. - pub const TOKEN_ALREADY_IN_USE: Win32Error = Win32Error(1375); - /// The specified local group does not exist. - pub const NO_SUCH_ALIAS: Win32Error = Win32Error(1376); - /// The specified account name is not a member of the group. - pub const MEMBER_NOT_IN_ALIAS: Win32Error = Win32Error(1377); - /// The specified account name is already a member of the group. - pub const MEMBER_IN_ALIAS: Win32Error = Win32Error(1378); - /// The specified local group already exists. - pub const ALIAS_EXISTS: Win32Error = Win32Error(1379); - /// Logon failure: the user has not been granted the requested logon type at this computer. - pub const LOGON_NOT_GRANTED: Win32Error = Win32Error(1380); - /// The maximum number of secrets that may be stored in a single system has been exceeded. - pub const TOO_MANY_SECRETS: Win32Error = Win32Error(1381); - /// The length of a secret exceeds the maximum length allowed. - pub const SECRET_TOO_LONG: Win32Error = Win32Error(1382); - /// The local security authority database contains an internal inconsistency. - pub const INTERNAL_DB_ERROR: Win32Error = Win32Error(1383); - /// During a logon attempt, the user's security context accumulated too many security IDs. - pub const TOO_MANY_CONTEXT_IDS: Win32Error = Win32Error(1384); - /// Logon failure: the user has not been granted the requested logon type at this computer. - pub const LOGON_TYPE_NOT_GRANTED: Win32Error = Win32Error(1385); - /// A cross-encrypted password is necessary to change a user password. - pub const NT_CROSS_ENCRYPTION_REQUIRED: Win32Error = Win32Error(1386); - /// A member could not be added to or removed from the local group because the member does not exist. - pub const NO_SUCH_MEMBER: Win32Error = Win32Error(1387); - /// A new member could not be added to a local group because the member has the wrong account type. - pub const INVALID_MEMBER: Win32Error = Win32Error(1388); - /// Too many security IDs have been specified. - pub const TOO_MANY_SIDS: Win32Error = Win32Error(1389); - /// A cross-encrypted password is necessary to change this user password. - pub const LM_CROSS_ENCRYPTION_REQUIRED: Win32Error = Win32Error(1390); - /// Indicates an ACL contains no inheritable components. - pub const NO_INHERITANCE: Win32Error = Win32Error(1391); - /// The file or directory is corrupted and unreadable. - pub const FILE_CORRUPT: Win32Error = Win32Error(1392); - /// The disk structure is corrupted and unreadable. - pub const DISK_CORRUPT: Win32Error = Win32Error(1393); - /// There is no user session key for the specified logon session. - pub const NO_USER_SESSION_KEY: Win32Error = Win32Error(1394); - /// The service being accessed is licensed for a particular number of connections. - /// No more connections can be made to the service at this time because there are already as many connections as the service can accept. - pub const LICENSE_QUOTA_EXCEEDED: Win32Error = Win32Error(1395); - /// The target account name is incorrect. - pub const WRONG_TARGET_NAME: Win32Error = Win32Error(1396); - /// Mutual Authentication failed. The server's password is out of date at the domain controller. - pub const MUTUAL_AUTH_FAILED: Win32Error = Win32Error(1397); - /// There is a time and/or date difference between the client and server. - pub const TIME_SKEW: Win32Error = Win32Error(1398); - /// This operation cannot be performed on the current domain. - pub const CURRENT_DOMAIN_NOT_ALLOWED: Win32Error = Win32Error(1399); - /// Invalid window handle. - pub const INVALID_WINDOW_HANDLE: Win32Error = Win32Error(1400); - /// Invalid menu handle. - pub const INVALID_MENU_HANDLE: Win32Error = Win32Error(1401); - /// Invalid cursor handle. - pub const INVALID_CURSOR_HANDLE: Win32Error = Win32Error(1402); - /// Invalid accelerator table handle. - pub const INVALID_ACCEL_HANDLE: Win32Error = Win32Error(1403); - /// Invalid hook handle. - pub const INVALID_HOOK_HANDLE: Win32Error = Win32Error(1404); - /// Invalid handle to a multiple-window position structure. - pub const INVALID_DWP_HANDLE: Win32Error = Win32Error(1405); - /// Cannot create a top-level child window. - pub const TLW_WITH_WSCHILD: Win32Error = Win32Error(1406); - /// Cannot find window class. - pub const CANNOT_FIND_WND_CLASS: Win32Error = Win32Error(1407); - /// Invalid window; it belongs to other thread. - pub const WINDOW_OF_OTHER_THREAD: Win32Error = Win32Error(1408); - /// Hot key is already registered. - pub const HOTKEY_ALREADY_REGISTERED: Win32Error = Win32Error(1409); - /// Class already exists. - pub const CLASS_ALREADY_EXISTS: Win32Error = Win32Error(1410); - /// Class does not exist. - pub const CLASS_DOES_NOT_EXIST: Win32Error = Win32Error(1411); - /// Class still has openwin32. - pub const CLASS_HAS_WINDOWS: Win32Error = Win32Error(1412); - /// Invalid index. - pub const INVALID_INDEX: Win32Error = Win32Error(1413); - /// Invalid icon handle. - pub const INVALID_ICON_HANDLE: Win32Error = Win32Error(1414); - /// Using private DIALOG window words. - pub const PRIVATE_DIALOG_INDEX: Win32Error = Win32Error(1415); - /// The list box identifier was not found. - pub const LISTBOX_ID_NOT_FOUND: Win32Error = Win32Error(1416); - /// No wildcards were found. - pub const NO_WILDCARD_CHARACTERS: Win32Error = Win32Error(1417); - /// Thread does not have a clipboard open. - pub const CLIPBOARD_NOT_OPEN: Win32Error = Win32Error(1418); - /// Hot key is not registered. - pub const HOTKEY_NOT_REGISTERED: Win32Error = Win32Error(1419); - /// The window is not a valid dialog window. - pub const WINDOW_NOT_DIALOG: Win32Error = Win32Error(1420); - /// Control ID not found. - pub const CONTROL_ID_NOT_FOUND: Win32Error = Win32Error(1421); - /// Invalid message for a combo box because it does not have an edit control. - pub const INVALID_COMBOBOX_MESSAGE: Win32Error = Win32Error(1422); - /// The window is not a combo box. - pub const WINDOW_NOT_COMBOBOX: Win32Error = Win32Error(1423); - /// Height must be less than 256. - pub const INVALID_EDIT_HEIGHT: Win32Error = Win32Error(1424); - /// Invalid device context (DC) handle. - pub const DC_NOT_FOUND: Win32Error = Win32Error(1425); - /// Invalid hook procedure type. - pub const INVALID_HOOK_FILTER: Win32Error = Win32Error(1426); - /// Invalid hook procedure. - pub const INVALID_FILTER_PROC: Win32Error = Win32Error(1427); - /// Cannot set nonlocal hook without a module handle. - pub const HOOK_NEEDS_HMOD: Win32Error = Win32Error(1428); - /// This hook procedure can only be set globally. - pub const GLOBAL_ONLY_HOOK: Win32Error = Win32Error(1429); - /// The journal hook procedure is already installed. - pub const JOURNAL_HOOK_SET: Win32Error = Win32Error(1430); - /// The hook procedure is not installed. - pub const HOOK_NOT_INSTALLED: Win32Error = Win32Error(1431); - /// Invalid message for single-selection list box. - pub const INVALID_LB_MESSAGE: Win32Error = Win32Error(1432); - /// LB_SETCOUNT sent to non-lazy list box. - pub const SETCOUNT_ON_BAD_LB: Win32Error = Win32Error(1433); - /// This list box does not support tab stops. - pub const LB_WITHOUT_TABSTOPS: Win32Error = Win32Error(1434); - /// Cannot destroy object created by another thread. - pub const DESTROY_OBJECT_OF_OTHER_THREAD: Win32Error = Win32Error(1435); - - /// The data present in the reparse point buffer is invalid. - pub const INVALID_REPARSE_DATA: Win32Error = Win32Error(3492); - - /// Childwin32.cannot have menus. - pub const CHILD_WINDOW_MENU: Win32Error = Win32Error(1436); - /// The window does not have a system menu. - pub const NO_SYSTEM_MENU: Win32Error = Win32Error(1437); - /// Invalid message box style. - pub const INVALID_MSGBOX_STYLE: Win32Error = Win32Error(1438); - /// Invalid system-wide (SPI_*) parameter. - pub const INVALID_SPI_VALUE: Win32Error = Win32Error(1439); - /// Screen already locked. - pub const SCREEN_ALREADY_LOCKED: Win32Error = Win32Error(1440); - /// All handles towin32.in a multiple-window position structure must have the same parent. - pub const HWNDS_HAVE_DIFF_PARENT: Win32Error = Win32Error(1441); - /// The window is not a child window. - pub const NOT_CHILD_WINDOW: Win32Error = Win32Error(1442); - /// Invalid GW_* command. - pub const INVALID_GW_COMMAND: Win32Error = Win32Error(1443); - /// Invalid thread identifier. - pub const INVALID_THREAD_ID: Win32Error = Win32Error(1444); - /// Cannot process a message from a window that is not a multiple document interface (MDI) window. - pub const NON_MDICHILD_WINDOW: Win32Error = Win32Error(1445); - /// Popup menu already active. - pub const POPUP_ALREADY_ACTIVE: Win32Error = Win32Error(1446); - /// The window does not have scroll bars. - pub const NO_SCROLLBARS: Win32Error = Win32Error(1447); - /// Scroll bar range cannot be greater than MAXLONG. - pub const INVALID_SCROLLBAR_RANGE: Win32Error = Win32Error(1448); - /// Cannot show or remove the window in the way specified. - pub const INVALID_SHOWWIN_COMMAND: Win32Error = Win32Error(1449); - /// Insufficient system resources exist to complete the requested service. - pub const NO_SYSTEM_RESOURCES: Win32Error = Win32Error(1450); - /// Insufficient system resources exist to complete the requested service. - pub const NONPAGED_SYSTEM_RESOURCES: Win32Error = Win32Error(1451); - /// Insufficient system resources exist to complete the requested service. - pub const PAGED_SYSTEM_RESOURCES: Win32Error = Win32Error(1452); - /// Insufficient quota to complete the requested service. - pub const WORKING_SET_QUOTA: Win32Error = Win32Error(1453); - /// Insufficient quota to complete the requested service. - pub const PAGEFILE_QUOTA: Win32Error = Win32Error(1454); - /// The paging file is too small for this operation to complete. - pub const COMMITMENT_LIMIT: Win32Error = Win32Error(1455); - /// A menu item was not found. - pub const MENU_ITEM_NOT_FOUND: Win32Error = Win32Error(1456); - /// Invalid keyboard layout handle. - pub const INVALID_KEYBOARD_HANDLE: Win32Error = Win32Error(1457); - /// Hook type not allowed. - pub const HOOK_TYPE_NOT_ALLOWED: Win32Error = Win32Error(1458); - /// This operation requires an interactive window station. - pub const REQUIRES_INTERACTIVE_WINDOWSTATION: Win32Error = Win32Error(1459); - /// This operation returned because the timeout period expired. - pub const TIMEOUT: Win32Error = Win32Error(1460); - /// Invalid monitor handle. - pub const INVALID_MONITOR_HANDLE: Win32Error = Win32Error(1461); - /// Incorrect size argument. - pub const INCORRECT_SIZE: Win32Error = Win32Error(1462); - /// The symbolic link cannot be followed because its type is disabled. - pub const SYMLINK_CLASS_DISABLED: Win32Error = Win32Error(1463); - /// This application does not support the current operation on symbolic links. - pub const SYMLINK_NOT_SUPPORTED: Win32Error = Win32Error(1464); - /// Windows was unable to parse the requested XML data. - pub const XML_PARSE_ERROR: Win32Error = Win32Error(1465); - /// An error was encountered while processing an XML digital signature. - pub const XMLDSIG_ERROR: Win32Error = Win32Error(1466); - /// This application must be restarted. - pub const RESTART_APPLICATION: Win32Error = Win32Error(1467); - /// The caller made the connection request in the wrong routing compartment. - pub const WRONG_COMPARTMENT: Win32Error = Win32Error(1468); - /// There was an AuthIP failure when attempting to connect to the remote host. - pub const AUTHIP_FAILURE: Win32Error = Win32Error(1469); - /// Insufficient NVRAM resources exist to complete the requested service. A reboot might be required. - pub const NO_NVRAM_RESOURCES: Win32Error = Win32Error(1470); - /// Unable to finish the requested operation because the specified process is not a GUI process. - pub const NOT_GUI_PROCESS: Win32Error = Win32Error(1471); - /// The event log file is corrupted. - pub const EVENTLOG_FILE_CORRUPT: Win32Error = Win32Error(1500); - /// No event log file could be opened, so the event logging service did not start. - pub const EVENTLOG_CANT_START: Win32Error = Win32Error(1501); - /// The event log file is full. - pub const LOG_FILE_FULL: Win32Error = Win32Error(1502); - /// The event log file has changed between read operations. - pub const EVENTLOG_FILE_CHANGED: Win32Error = Win32Error(1503); - /// The specified task name is invalid. - pub const INVALID_TASK_NAME: Win32Error = Win32Error(1550); - /// The specified task index is invalid. - pub const INVALID_TASK_INDEX: Win32Error = Win32Error(1551); - /// The specified thread is already joining a task. - pub const THREAD_ALREADY_IN_TASK: Win32Error = Win32Error(1552); - /// The Windows Installer Service could not be accessed. - /// This can occur if the Windows Installer is not correctly installed. Contact your support personnel for assistance. - pub const INSTALL_SERVICE_FAILURE: Win32Error = Win32Error(1601); - /// User cancelled installation. - pub const INSTALL_USEREXIT: Win32Error = Win32Error(1602); - /// Fatal error during installation. - pub const INSTALL_FAILURE: Win32Error = Win32Error(1603); - /// Installation suspended, incomplete. - pub const INSTALL_SUSPEND: Win32Error = Win32Error(1604); - /// This action is only valid for products that are currently installed. - pub const UNKNOWN_PRODUCT: Win32Error = Win32Error(1605); - /// Feature ID not registered. - pub const UNKNOWN_FEATURE: Win32Error = Win32Error(1606); - /// Component ID not registered. - pub const UNKNOWN_COMPONENT: Win32Error = Win32Error(1607); - /// Unknown property. - pub const UNKNOWN_PROPERTY: Win32Error = Win32Error(1608); - /// Handle is in an invalid state. - pub const INVALID_HANDLE_STATE: Win32Error = Win32Error(1609); - /// The configuration data for this product is corrupt. Contact your support personnel. - pub const BAD_CONFIGURATION: Win32Error = Win32Error(1610); - /// Component qualifier not present. - pub const INDEX_ABSENT: Win32Error = Win32Error(1611); - /// The installation source for this product is not available. - /// Verify that the source exists and that you can access it. - pub const INSTALL_SOURCE_ABSENT: Win32Error = Win32Error(1612); - /// This installation package cannot be installed by the Windows Installer service. - /// You must install a Windows service pack that contains a newer version of the Windows Installer service. - pub const INSTALL_PACKAGE_VERSION: Win32Error = Win32Error(1613); - /// Product is uninstalled. - pub const PRODUCT_UNINSTALLED: Win32Error = Win32Error(1614); - /// SQL query syntax invalid or unsupported. - pub const BAD_QUERY_SYNTAX: Win32Error = Win32Error(1615); - /// Record field does not exist. - pub const INVALID_FIELD: Win32Error = Win32Error(1616); - /// The device has been removed. - pub const DEVICE_REMOVED: Win32Error = Win32Error(1617); - /// Another installation is already in progress. - /// Complete that installation before proceeding with this install. - pub const INSTALL_ALREADY_RUNNING: Win32Error = Win32Error(1618); - /// This installation package could not be opened. - /// Verify that the package exists and that you can access it, or contact the application vendor to verify that this is a valid Windows Installer package. - pub const INSTALL_PACKAGE_OPEN_FAILED: Win32Error = Win32Error(1619); - /// This installation package could not be opened. - /// Contact the application vendor to verify that this is a valid Windows Installer package. - pub const INSTALL_PACKAGE_INVALID: Win32Error = Win32Error(1620); - /// There was an error starting the Windows Installer service user interface. Contact your support personnel. - pub const INSTALL_UI_FAILURE: Win32Error = Win32Error(1621); - /// Error opening installation log file. - /// Verify that the specified log file location exists and that you can write to it. - pub const INSTALL_LOG_FAILURE: Win32Error = Win32Error(1622); - /// The language of this installation package is not supported by your system. - pub const INSTALL_LANGUAGE_UNSUPPORTED: Win32Error = Win32Error(1623); - /// Error applying transforms. Verify that the specified transform paths are valid. - pub const INSTALL_TRANSFORM_FAILURE: Win32Error = Win32Error(1624); - /// This installation is forbidden by system policy. Contact your system administrator. - pub const INSTALL_PACKAGE_REJECTED: Win32Error = Win32Error(1625); - /// Function could not be executed. - pub const FUNCTION_NOT_CALLED: Win32Error = Win32Error(1626); - /// Function failed during execution. - pub const FUNCTION_FAILED: Win32Error = Win32Error(1627); - /// Invalid or unknown table specified. - pub const INVALID_TABLE: Win32Error = Win32Error(1628); - /// Data supplied is of wrong type. - pub const DATATYPE_MISMATCH: Win32Error = Win32Error(1629); - /// Data of this type is not supported. - pub const UNSUPPORTED_TYPE: Win32Error = Win32Error(1630); - /// The Windows Installer service failed to start. Contact your support personnel. - pub const CREATE_FAILED: Win32Error = Win32Error(1631); - /// The Temp folder is on a drive that is full or is inaccessible. - /// Free up space on the drive or verify that you have write permission on the Temp folder. - pub const INSTALL_TEMP_UNWRITABLE: Win32Error = Win32Error(1632); - /// This installation package is not supported by this processor type. Contact your product vendor. - pub const INSTALL_PLATFORM_UNSUPPORTED: Win32Error = Win32Error(1633); - /// Component not used on this computer. - pub const INSTALL_NOTUSED: Win32Error = Win32Error(1634); - /// This update package could not be opened. - /// Verify that the update package exists and that you can access it, or contact the application vendor to verify that this is a valid Windows Installer update package. - pub const PATCH_PACKAGE_OPEN_FAILED: Win32Error = Win32Error(1635); - /// This update package could not be opened. - /// Contact the application vendor to verify that this is a valid Windows Installer update package. - pub const PATCH_PACKAGE_INVALID: Win32Error = Win32Error(1636); - /// This update package cannot be processed by the Windows Installer service. - /// You must install a Windows service pack that contains a newer version of the Windows Installer service. - pub const PATCH_PACKAGE_UNSUPPORTED: Win32Error = Win32Error(1637); - /// Another version of this product is already installed. Installation of this version cannot continue. - /// To configure or remove the existing version of this product, use Add/Remove Programs on the Control Panel. - pub const PRODUCT_VERSION: Win32Error = Win32Error(1638); - /// Invalid command line argument. Consult the Windows Installer SDK for detailed command line help. - pub const INVALID_COMMAND_LINE: Win32Error = Win32Error(1639); - /// Only administrators have permission to add, remove, or configure server software during a Terminal services remote session. - /// If you want to install or configure software on the server, contact your network administrator. - pub const INSTALL_REMOTE_DISALLOWED: Win32Error = Win32Error(1640); - /// The requested operation completed successfully. - /// The system will be restarted so the changes can take effect. - pub const SUCCESS_REBOOT_INITIATED: Win32Error = Win32Error(1641); - /// The upgrade cannot be installed by the Windows Installer service because the program to be upgraded may be missing, or the upgrade may update a different version of the program. - /// Verify that the program to be upgraded exists on your computer and that you have the correct upgrade. - pub const PATCH_TARGET_NOT_FOUND: Win32Error = Win32Error(1642); - /// The update package is not permitted by software restriction policy. - pub const PATCH_PACKAGE_REJECTED: Win32Error = Win32Error(1643); - /// One or more customizations are not permitted by software restriction policy. - pub const INSTALL_TRANSFORM_REJECTED: Win32Error = Win32Error(1644); - /// The Windows Installer does not permit installation from a Remote Desktop Connection. - pub const INSTALL_REMOTE_PROHIBITED: Win32Error = Win32Error(1645); - /// Uninstallation of the update package is not supported. - pub const PATCH_REMOVAL_UNSUPPORTED: Win32Error = Win32Error(1646); - /// The update is not applied to this product. - pub const UNKNOWN_PATCH: Win32Error = Win32Error(1647); - /// No valid sequence could be found for the set of updates. - pub const PATCH_NO_SEQUENCE: Win32Error = Win32Error(1648); - /// Update removal was disallowed by policy. - pub const PATCH_REMOVAL_DISALLOWED: Win32Error = Win32Error(1649); - /// The XML update data is invalid. - pub const INVALID_PATCH_XML: Win32Error = Win32Error(1650); - /// Windows Installer does not permit updating of managed advertised products. - /// At least one feature of the product must be installed before applying the update. - pub const PATCH_MANAGED_ADVERTISED_PRODUCT: Win32Error = Win32Error(1651); - /// The Windows Installer service is not accessible in Safe Mode. - /// Please try again when your computer is not in Safe Mode or you can use System Restore to return your machine to a previous good state. - pub const INSTALL_SERVICE_SAFEBOOT: Win32Error = Win32Error(1652); - /// A fail fast exception occurred. - /// Exception handlers will not be invoked and the process will be terminated immediately. - pub const FAIL_FAST_EXCEPTION: Win32Error = Win32Error(1653); - /// The app that you are trying to run is not supported on this version of Windows. - pub const INSTALL_REJECTED: Win32Error = Win32Error(1654); - /// The string binding is invalid. - pub const RPC_S_INVALID_STRING_BINDING: Win32Error = Win32Error(1700); - /// The binding handle is not the correct type. - pub const RPC_S_WRONG_KIND_OF_BINDING: Win32Error = Win32Error(1701); - /// The binding handle is invalid. - pub const RPC_S_INVALID_BINDING: Win32Error = Win32Error(1702); - /// The RPC protocol sequence is not supported. - pub const RPC_S_PROTSEQ_NOT_SUPPORTED: Win32Error = Win32Error(1703); - /// The RPC protocol sequence is invalid. - pub const RPC_S_INVALID_RPC_PROTSEQ: Win32Error = Win32Error(1704); - /// The string universal unique identifier (UUID) is invalid. - pub const RPC_S_INVALID_STRING_UUID: Win32Error = Win32Error(1705); - /// The endpoint format is invalid. - pub const RPC_S_INVALID_ENDPOINT_FORMAT: Win32Error = Win32Error(1706); - /// The network address is invalid. - pub const RPC_S_INVALID_NET_ADDR: Win32Error = Win32Error(1707); - /// No endpoint was found. - pub const RPC_S_NO_ENDPOINT_FOUND: Win32Error = Win32Error(1708); - /// The timeout value is invalid. - pub const RPC_S_INVALID_TIMEOUT: Win32Error = Win32Error(1709); - /// The object universal unique identifier (UUID) was not found. - pub const RPC_S_OBJECT_NOT_FOUND: Win32Error = Win32Error(1710); - /// The object universal unique identifier (UUID) has already been registered. - pub const RPC_S_ALREADY_REGISTERED: Win32Error = Win32Error(1711); - /// The type universal unique identifier (UUID) has already been registered. - pub const RPC_S_TYPE_ALREADY_REGISTERED: Win32Error = Win32Error(1712); - /// The RPC server is already listening. - pub const RPC_S_ALREADY_LISTENING: Win32Error = Win32Error(1713); - /// No protocol sequences have been registered. - pub const RPC_S_NO_PROTSEQS_REGISTERED: Win32Error = Win32Error(1714); - /// The RPC server is not listening. - pub const RPC_S_NOT_LISTENING: Win32Error = Win32Error(1715); - /// The manager type is unknown. - pub const RPC_S_UNKNOWN_MGR_TYPE: Win32Error = Win32Error(1716); - /// The interface is unknown. - pub const RPC_S_UNKNOWN_IF: Win32Error = Win32Error(1717); - /// There are no bindings. - pub const RPC_S_NO_BINDINGS: Win32Error = Win32Error(1718); - /// There are no protocol sequences. - pub const RPC_S_NO_PROTSEQS: Win32Error = Win32Error(1719); - /// The endpoint cannot be created. - pub const RPC_S_CANT_CREATE_ENDPOINT: Win32Error = Win32Error(1720); - /// Not enough resources are available to complete this operation. - pub const RPC_S_OUT_OF_RESOURCES: Win32Error = Win32Error(1721); - /// The RPC server is unavailable. - pub const RPC_S_SERVER_UNAVAILABLE: Win32Error = Win32Error(1722); - /// The RPC server is too busy to complete this operation. - pub const RPC_S_SERVER_TOO_BUSY: Win32Error = Win32Error(1723); - /// The network options are invalid. - pub const RPC_S_INVALID_NETWORK_OPTIONS: Win32Error = Win32Error(1724); - /// There are no remote procedure calls active on this thread. - pub const RPC_S_NO_CALL_ACTIVE: Win32Error = Win32Error(1725); - /// The remote procedure call failed. - pub const RPC_S_CALL_FAILED: Win32Error = Win32Error(1726); - /// The remote procedure call failed and did not execute. - pub const RPC_S_CALL_FAILED_DNE: Win32Error = Win32Error(1727); - /// A remote procedure call (RPC) protocol error occurred. - pub const RPC_S_PROTOCOL_ERROR: Win32Error = Win32Error(1728); - /// Access to the HTTP proxy is denied. - pub const RPC_S_PROXY_ACCESS_DENIED: Win32Error = Win32Error(1729); - /// The transfer syntax is not supported by the RPC server. - pub const RPC_S_UNSUPPORTED_TRANS_SYN: Win32Error = Win32Error(1730); - /// The universal unique identifier (UUID) type is not supported. - pub const RPC_S_UNSUPPORTED_TYPE: Win32Error = Win32Error(1732); - /// The tag is invalid. - pub const RPC_S_INVALID_TAG: Win32Error = Win32Error(1733); - /// The array bounds are invalid. - pub const RPC_S_INVALID_BOUND: Win32Error = Win32Error(1734); - /// The binding does not contain an entry name. - pub const RPC_S_NO_ENTRY_NAME: Win32Error = Win32Error(1735); - /// The name syntax is invalid. - pub const RPC_S_INVALID_NAME_SYNTAX: Win32Error = Win32Error(1736); - /// The name syntax is not supported. - pub const RPC_S_UNSUPPORTED_NAME_SYNTAX: Win32Error = Win32Error(1737); - /// No network address is available to use to construct a universal unique identifier (UUID). - pub const RPC_S_UUID_NO_ADDRESS: Win32Error = Win32Error(1739); - /// The endpoint is a duplicate. - pub const RPC_S_DUPLICATE_ENDPOINT: Win32Error = Win32Error(1740); - /// The authentication type is unknown. - pub const RPC_S_UNKNOWN_AUTHN_TYPE: Win32Error = Win32Error(1741); - /// The maximum number of calls is too small. - pub const RPC_S_MAX_CALLS_TOO_SMALL: Win32Error = Win32Error(1742); - /// The string is too long. - pub const RPC_S_STRING_TOO_LONG: Win32Error = Win32Error(1743); - /// The RPC protocol sequence was not found. - pub const RPC_S_PROTSEQ_NOT_FOUND: Win32Error = Win32Error(1744); - /// The procedure number is out of range. - pub const RPC_S_PROCNUM_OUT_OF_RANGE: Win32Error = Win32Error(1745); - /// The binding does not contain any authentication information. - pub const RPC_S_BINDING_HAS_NO_AUTH: Win32Error = Win32Error(1746); - /// The authentication service is unknown. - pub const RPC_S_UNKNOWN_AUTHN_SERVICE: Win32Error = Win32Error(1747); - /// The authentication level is unknown. - pub const RPC_S_UNKNOWN_AUTHN_LEVEL: Win32Error = Win32Error(1748); - /// The security context is invalid. - pub const RPC_S_INVALID_AUTH_IDENTITY: Win32Error = Win32Error(1749); - /// The authorization service is unknown. - pub const RPC_S_UNKNOWN_AUTHZ_SERVICE: Win32Error = Win32Error(1750); - /// The entry is invalid. - pub const EPT_S_INVALID_ENTRY: Win32Error = Win32Error(1751); - /// The server endpoint cannot perform the operation. - pub const EPT_S_CANT_PERFORM_OP: Win32Error = Win32Error(1752); - /// There are no more endpoints available from the endpoint mapper. - pub const EPT_S_NOT_REGISTERED: Win32Error = Win32Error(1753); - /// No interfaces have been exported. - pub const RPC_S_NOTHING_TO_EXPORT: Win32Error = Win32Error(1754); - /// The entry name is incomplete. - pub const RPC_S_INCOMPLETE_NAME: Win32Error = Win32Error(1755); - /// The version option is invalid. - pub const RPC_S_INVALID_VERS_OPTION: Win32Error = Win32Error(1756); - /// There are no more members. - pub const RPC_S_NO_MORE_MEMBERS: Win32Error = Win32Error(1757); - /// There is nothing to unexport. - pub const RPC_S_NOT_ALL_OBJS_UNEXPORTED: Win32Error = Win32Error(1758); - /// The interface was not found. - pub const RPC_S_INTERFACE_NOT_FOUND: Win32Error = Win32Error(1759); - /// The entry already exists. - pub const RPC_S_ENTRY_ALREADY_EXISTS: Win32Error = Win32Error(1760); - /// The entry is not found. - pub const RPC_S_ENTRY_NOT_FOUND: Win32Error = Win32Error(1761); - /// The name service is unavailable. - pub const RPC_S_NAME_SERVICE_UNAVAILABLE: Win32Error = Win32Error(1762); - /// The network address family is invalid. - pub const RPC_S_INVALID_NAF_ID: Win32Error = Win32Error(1763); - /// The requested operation is not supported. - pub const RPC_S_CANNOT_SUPPORT: Win32Error = Win32Error(1764); - /// No security context is available to allow impersonation. - pub const RPC_S_NO_CONTEXT_AVAILABLE: Win32Error = Win32Error(1765); - /// An internal error occurred in a remote procedure call (RPC). - pub const RPC_S_INTERNAL_ERROR: Win32Error = Win32Error(1766); - /// The RPC server attempted an integer division by zero. - pub const RPC_S_ZERO_DIVIDE: Win32Error = Win32Error(1767); - /// An addressing error occurred in the RPC server. - pub const RPC_S_ADDRESS_ERROR: Win32Error = Win32Error(1768); - /// A floating-point operation at the RPC server caused a division by zero. - pub const RPC_S_FP_DIV_ZERO: Win32Error = Win32Error(1769); - /// A floating-point underflow occurred at the RPC server. - pub const RPC_S_FP_UNDERFLOW: Win32Error = Win32Error(1770); - /// A floating-point overflow occurred at the RPC server. - pub const RPC_S_FP_OVERFLOW: Win32Error = Win32Error(1771); - /// The list of RPC servers available for the binding of auto handles has been exhausted. - pub const RPC_X_NO_MORE_ENTRIES: Win32Error = Win32Error(1772); - /// Unable to open the character translation table file. - pub const RPC_X_SS_CHAR_TRANS_OPEN_FAIL: Win32Error = Win32Error(1773); - /// The file containing the character translation table has fewer than 512 bytes. - pub const RPC_X_SS_CHAR_TRANS_SHORT_FILE: Win32Error = Win32Error(1774); - /// A null context handle was passed from the client to the host during a remote procedure call. - pub const RPC_X_SS_IN_NULL_CONTEXT: Win32Error = Win32Error(1775); - /// The context handle changed during a remote procedure call. - pub const RPC_X_SS_CONTEXT_DAMAGED: Win32Error = Win32Error(1777); - /// The binding handles passed to a remote procedure call do not match. - pub const RPC_X_SS_HANDLES_MISMATCH: Win32Error = Win32Error(1778); - /// The stub is unable to get the remote procedure call handle. - pub const RPC_X_SS_CANNOT_GET_CALL_HANDLE: Win32Error = Win32Error(1779); - /// A null reference pointer was passed to the stub. - pub const RPC_X_NULL_REF_POINTER: Win32Error = Win32Error(1780); - /// The enumeration value is out of range. - pub const RPC_X_ENUM_VALUE_OUT_OF_RANGE: Win32Error = Win32Error(1781); - /// The byte count is too small. - pub const RPC_X_BYTE_COUNT_TOO_SMALL: Win32Error = Win32Error(1782); - /// The stub received bad data. - pub const RPC_X_BAD_STUB_DATA: Win32Error = Win32Error(1783); - /// The supplied user buffer is not valid for the requested operation. - pub const INVALID_USER_BUFFER: Win32Error = Win32Error(1784); - /// The disk media is not recognized. It may not be formatted. - pub const UNRECOGNIZED_MEDIA: Win32Error = Win32Error(1785); - /// The workstation does not have a trust secret. - pub const NO_TRUST_LSA_SECRET: Win32Error = Win32Error(1786); - /// The security database on the server does not have a computer account for this workstation trust relationship. - pub const NO_TRUST_SAM_ACCOUNT: Win32Error = Win32Error(1787); - /// The trust relationship between the primary domain and the trusted domain failed. - pub const TRUSTED_DOMAIN_FAILURE: Win32Error = Win32Error(1788); - /// The trust relationship between this workstation and the primary domain failed. - pub const TRUSTED_RELATIONSHIP_FAILURE: Win32Error = Win32Error(1789); - /// The network logon failed. - pub const TRUST_FAILURE: Win32Error = Win32Error(1790); - /// A remote procedure call is already in progress for this thread. - pub const RPC_S_CALL_IN_PROGRESS: Win32Error = Win32Error(1791); - /// An attempt was made to logon, but the network logon service was not started. - pub const NETLOGON_NOT_STARTED: Win32Error = Win32Error(1792); - /// The user's account has expired. - pub const ACCOUNT_EXPIRED: Win32Error = Win32Error(1793); - /// The redirector is in use and cannot be unloaded. - pub const REDIRECTOR_HAS_OPEN_HANDLES: Win32Error = Win32Error(1794); - /// The specified printer driver is already installed. - pub const PRINTER_DRIVER_ALREADY_INSTALLED: Win32Error = Win32Error(1795); - /// The specified port is unknown. - pub const UNKNOWN_PORT: Win32Error = Win32Error(1796); - /// The printer driver is unknown. - pub const UNKNOWN_PRINTER_DRIVER: Win32Error = Win32Error(1797); - /// The print processor is unknown. - pub const UNKNOWN_PRINTPROCESSOR: Win32Error = Win32Error(1798); - /// The specified separator file is invalid. - pub const INVALID_SEPARATOR_FILE: Win32Error = Win32Error(1799); - /// The specified priority is invalid. - pub const INVALID_PRIORITY: Win32Error = Win32Error(1800); - /// The printer name is invalid. - pub const INVALID_PRINTER_NAME: Win32Error = Win32Error(1801); - /// The printer already exists. - pub const PRINTER_ALREADY_EXISTS: Win32Error = Win32Error(1802); - /// The printer command is invalid. - pub const INVALID_PRINTER_COMMAND: Win32Error = Win32Error(1803); - /// The specified datatype is invalid. - pub const INVALID_DATATYPE: Win32Error = Win32Error(1804); - /// The environment specified is invalid. - pub const INVALID_ENVIRONMENT: Win32Error = Win32Error(1805); - /// There are no more bindings. - pub const RPC_S_NO_MORE_BINDINGS: Win32Error = Win32Error(1806); - /// The account used is an interdomain trust account. - /// Use your global user account or local user account to access this server. - pub const NOLOGON_INTERDOMAIN_TRUST_ACCOUNT: Win32Error = Win32Error(1807); - /// The account used is a computer account. - /// Use your global user account or local user account to access this server. - pub const NOLOGON_WORKSTATION_TRUST_ACCOUNT: Win32Error = Win32Error(1808); - /// The account used is a server trust account. - /// Use your global user account or local user account to access this server. - pub const NOLOGON_SERVER_TRUST_ACCOUNT: Win32Error = Win32Error(1809); - /// The name or security ID (SID) of the domain specified is inconsistent with the trust information for that domain. - pub const DOMAIN_TRUST_INCONSISTENT: Win32Error = Win32Error(1810); - /// The server is in use and cannot be unloaded. - pub const SERVER_HAS_OPEN_HANDLES: Win32Error = Win32Error(1811); - /// The specified image file did not contain a resource section. - pub const RESOURCE_DATA_NOT_FOUND: Win32Error = Win32Error(1812); - /// The specified resource type cannot be found in the image file. - pub const RESOURCE_TYPE_NOT_FOUND: Win32Error = Win32Error(1813); - /// The specified resource name cannot be found in the image file. - pub const RESOURCE_NAME_NOT_FOUND: Win32Error = Win32Error(1814); - /// The specified resource language ID cannot be found in the image file. - pub const RESOURCE_LANG_NOT_FOUND: Win32Error = Win32Error(1815); - /// Not enough quota is available to process this command. - pub const NOT_ENOUGH_QUOTA: Win32Error = Win32Error(1816); - /// No interfaces have been registered. - pub const RPC_S_NO_INTERFACES: Win32Error = Win32Error(1817); - /// The remote procedure call was cancelled. - pub const RPC_S_CALL_CANCELLED: Win32Error = Win32Error(1818); - /// The binding handle does not contain all required information. - pub const RPC_S_BINDING_INCOMPLETE: Win32Error = Win32Error(1819); - /// A communications failure occurred during a remote procedure call. - pub const RPC_S_COMM_FAILURE: Win32Error = Win32Error(1820); - /// The requested authentication level is not supported. - pub const RPC_S_UNSUPPORTED_AUTHN_LEVEL: Win32Error = Win32Error(1821); - /// No principal name registered. - pub const RPC_S_NO_PRINC_NAME: Win32Error = Win32Error(1822); - /// The error specified is not a valid Windows RPC error code. - pub const RPC_S_NOT_RPC_ERROR: Win32Error = Win32Error(1823); - /// A UUID that is valid only on this computer has been allocated. - pub const RPC_S_UUID_LOCAL_ONLY: Win32Error = Win32Error(1824); - /// A security package specific error occurred. - pub const RPC_S_SEC_PKG_ERROR: Win32Error = Win32Error(1825); - /// Thread is not canceled. - pub const RPC_S_NOT_CANCELLED: Win32Error = Win32Error(1826); - /// Invalid operation on the encoding/decoding handle. - pub const RPC_X_INVALID_ES_ACTION: Win32Error = Win32Error(1827); - /// Incompatible version of the serializing package. - pub const RPC_X_WRONG_ES_VERSION: Win32Error = Win32Error(1828); - /// Incompatible version of the RPC stub. - pub const RPC_X_WRONG_STUB_VERSION: Win32Error = Win32Error(1829); - /// The RPC pipe object is invalid or corrupted. - pub const RPC_X_INVALID_PIPE_OBJECT: Win32Error = Win32Error(1830); - /// An invalid operation was attempted on an RPC pipe object. - pub const RPC_X_WRONG_PIPE_ORDER: Win32Error = Win32Error(1831); - /// Unsupported RPC pipe version. - pub const RPC_X_WRONG_PIPE_VERSION: Win32Error = Win32Error(1832); - /// HTTP proxy server rejected the connection because the cookie authentication failed. - pub const RPC_S_COOKIE_AUTH_FAILED: Win32Error = Win32Error(1833); - /// The group member was not found. - pub const RPC_S_GROUP_MEMBER_NOT_FOUND: Win32Error = Win32Error(1898); - /// The endpoint mapper database entry could not be created. - pub const EPT_S_CANT_CREATE: Win32Error = Win32Error(1899); - /// The object universal unique identifier (UUID) is the nil UUID. - pub const RPC_S_INVALID_OBJECT: Win32Error = Win32Error(1900); - /// The specified time is invalid. - pub const INVALID_TIME: Win32Error = Win32Error(1901); - /// The specified form name is invalid. - pub const INVALID_FORM_NAME: Win32Error = Win32Error(1902); - /// The specified form size is invalid. - pub const INVALID_FORM_SIZE: Win32Error = Win32Error(1903); - /// The specified printer handle is already being waited on. - pub const ALREADY_WAITING: Win32Error = Win32Error(1904); - /// The specified printer has been deleted. - pub const PRINTER_DELETED: Win32Error = Win32Error(1905); - /// The state of the printer is invalid. - pub const INVALID_PRINTER_STATE: Win32Error = Win32Error(1906); - /// The user's password must be changed before signing in. - pub const PASSWORD_MUST_CHANGE: Win32Error = Win32Error(1907); - /// Could not find the domain controller for this domain. - pub const DOMAIN_CONTROLLER_NOT_FOUND: Win32Error = Win32Error(1908); - /// The referenced account is currently locked out and may not be logged on to. - pub const ACCOUNT_LOCKED_OUT: Win32Error = Win32Error(1909); - /// The object exporter specified was not found. - pub const OR_INVALID_OXID: Win32Error = Win32Error(1910); - /// The object specified was not found. - pub const OR_INVALID_OID: Win32Error = Win32Error(1911); - /// The object resolver set specified was not found. - pub const OR_INVALID_SET: Win32Error = Win32Error(1912); - /// Some data remains to be sent in the request buffer. - pub const RPC_S_SEND_INCOMPLETE: Win32Error = Win32Error(1913); - /// Invalid asynchronous remote procedure call handle. - pub const RPC_S_INVALID_ASYNC_HANDLE: Win32Error = Win32Error(1914); - /// Invalid asynchronous RPC call handle for this operation. - pub const RPC_S_INVALID_ASYNC_CALL: Win32Error = Win32Error(1915); - /// The RPC pipe object has already been closed. - pub const RPC_X_PIPE_CLOSED: Win32Error = Win32Error(1916); - /// The RPC call completed before all pipes were processed. - pub const RPC_X_PIPE_DISCIPLINE_ERROR: Win32Error = Win32Error(1917); - /// No more data is available from the RPC pipe. - pub const RPC_X_PIPE_EMPTY: Win32Error = Win32Error(1918); - /// No site name is available for this machine. - pub const NO_SITENAME: Win32Error = Win32Error(1919); - /// The file cannot be accessed by the system. - pub const CANT_ACCESS_FILE: Win32Error = Win32Error(1920); - /// The name of the file cannot be resolved by the system. - pub const CANT_RESOLVE_FILENAME: Win32Error = Win32Error(1921); - /// The entry is not of the expected type. - pub const RPC_S_ENTRY_TYPE_MISMATCH: Win32Error = Win32Error(1922); - /// Not all object UUIDs could be exported to the specified entry. - pub const RPC_S_NOT_ALL_OBJS_EXPORTED: Win32Error = Win32Error(1923); - /// Interface could not be exported to the specified entry. - pub const RPC_S_INTERFACE_NOT_EXPORTED: Win32Error = Win32Error(1924); - /// The specified profile entry could not be added. - pub const RPC_S_PROFILE_NOT_ADDED: Win32Error = Win32Error(1925); - /// The specified profile element could not be added. - pub const RPC_S_PRF_ELT_NOT_ADDED: Win32Error = Win32Error(1926); - /// The specified profile element could not be removed. - pub const RPC_S_PRF_ELT_NOT_REMOVED: Win32Error = Win32Error(1927); - /// The group element could not be added. - pub const RPC_S_GRP_ELT_NOT_ADDED: Win32Error = Win32Error(1928); - /// The group element could not be removed. - pub const RPC_S_GRP_ELT_NOT_REMOVED: Win32Error = Win32Error(1929); - /// The printer driver is not compatible with a policy enabled on your computer that blocks NT 4.0 drivers. - pub const KM_DRIVER_BLOCKED: Win32Error = Win32Error(1930); - /// The context has expired and can no longer be used. - pub const CONTEXT_EXPIRED: Win32Error = Win32Error(1931); - /// The current user's delegated trust creation quota has been exceeded. - pub const PER_USER_TRUST_QUOTA_EXCEEDED: Win32Error = Win32Error(1932); - /// The total delegated trust creation quota has been exceeded. - pub const ALL_USER_TRUST_QUOTA_EXCEEDED: Win32Error = Win32Error(1933); - /// The current user's delegated trust deletion quota has been exceeded. - pub const USER_DELETE_TRUST_QUOTA_EXCEEDED: Win32Error = Win32Error(1934); - /// The computer you are signing into is protected by an authentication firewall. - /// The specified account is not allowed to authenticate to the computer. - pub const AUTHENTICATION_FIREWALL_FAILED: Win32Error = Win32Error(1935); - /// Remote connections to the Print Spooler are blocked by a policy set on your machine. - pub const REMOTE_PRINT_CONNECTIONS_BLOCKED: Win32Error = Win32Error(1936); - /// Authentication failed because NTLM authentication has been disabled. - pub const NTLM_BLOCKED: Win32Error = Win32Error(1937); - /// Logon Failure: EAS policy requires that the user change their password before this operation can be performed. - pub const PASSWORD_CHANGE_REQUIRED: Win32Error = Win32Error(1938); - /// The pixel format is invalid. - pub const INVALID_PIXEL_FORMAT: Win32Error = Win32Error(2000); - /// The specified driver is invalid. - pub const BAD_DRIVER: Win32Error = Win32Error(2001); - /// The window style or class attribute is invalid for this operation. - pub const INVALID_WINDOW_STYLE: Win32Error = Win32Error(2002); - /// The requested metafile operation is not supported. - pub const METAFILE_NOT_SUPPORTED: Win32Error = Win32Error(2003); - /// The requested transformation operation is not supported. - pub const TRANSFORM_NOT_SUPPORTED: Win32Error = Win32Error(2004); - /// The requested clipping operation is not supported. - pub const CLIPPING_NOT_SUPPORTED: Win32Error = Win32Error(2005); - /// The specified color management module is invalid. - pub const INVALID_CMM: Win32Error = Win32Error(2010); - /// The specified color profile is invalid. - pub const INVALID_PROFILE: Win32Error = Win32Error(2011); - /// The specified tag was not found. - pub const TAG_NOT_FOUND: Win32Error = Win32Error(2012); - /// A required tag is not present. - pub const TAG_NOT_PRESENT: Win32Error = Win32Error(2013); - /// The specified tag is already present. - pub const DUPLICATE_TAG: Win32Error = Win32Error(2014); - /// The specified color profile is not associated with the specified device. - pub const PROFILE_NOT_ASSOCIATED_WITH_DEVICE: Win32Error = Win32Error(2015); - /// The specified color profile was not found. - pub const PROFILE_NOT_FOUND: Win32Error = Win32Error(2016); - /// The specified color space is invalid. - pub const INVALID_COLORSPACE: Win32Error = Win32Error(2017); - /// Image Color Management is not enabled. - pub const ICM_NOT_ENABLED: Win32Error = Win32Error(2018); - /// There was an error while deleting the color transform. - pub const DELETING_ICM_XFORM: Win32Error = Win32Error(2019); - /// The specified color transform is invalid. - pub const INVALID_TRANSFORM: Win32Error = Win32Error(2020); - /// The specified transform does not match the bitmap's color space. - pub const COLORSPACE_MISMATCH: Win32Error = Win32Error(2021); - /// The specified named color index is not present in the profile. - pub const INVALID_COLORINDEX: Win32Error = Win32Error(2022); - /// The specified profile is intended for a device of a different type than the specified device. - pub const PROFILE_DOES_NOT_MATCH_DEVICE: Win32Error = Win32Error(2023); - /// The network connection was made successfully, but the user had to be prompted for a password other than the one originally specified. - pub const CONNECTED_OTHER_PASSWORD: Win32Error = Win32Error(2108); - /// The network connection was made successfully using default credentials. - pub const CONNECTED_OTHER_PASSWORD_DEFAULT: Win32Error = Win32Error(2109); - /// The specified username is invalid. - pub const BAD_USERNAME: Win32Error = Win32Error(2202); - /// This network connection does not exist. - pub const NOT_CONNECTED: Win32Error = Win32Error(2250); - /// This network connection has files open or requests pending. - pub const OPEN_FILES: Win32Error = Win32Error(2401); - /// Active connections still exist. - pub const ACTIVE_CONNECTIONS: Win32Error = Win32Error(2402); - /// The device is in use by an active process and cannot be disconnected. - pub const DEVICE_IN_USE: Win32Error = Win32Error(2404); - /// The specified print monitor is unknown. - pub const UNKNOWN_PRINT_MONITOR: Win32Error = Win32Error(3000); - /// The specified printer driver is currently in use. - pub const PRINTER_DRIVER_IN_USE: Win32Error = Win32Error(3001); - /// The spool file was not found. - pub const SPOOL_FILE_NOT_FOUND: Win32Error = Win32Error(3002); - /// A StartDocPrinter call was not issued. - pub const SPL_NO_STARTDOC: Win32Error = Win32Error(3003); - /// An AddJob call was not issued. - pub const SPL_NO_ADDJOB: Win32Error = Win32Error(3004); - /// The specified print processor has already been installed. - pub const PRINT_PROCESSOR_ALREADY_INSTALLED: Win32Error = Win32Error(3005); - /// The specified print monitor has already been installed. - pub const PRINT_MONITOR_ALREADY_INSTALLED: Win32Error = Win32Error(3006); - /// The specified print monitor does not have the required functions. - pub const INVALID_PRINT_MONITOR: Win32Error = Win32Error(3007); - /// The specified print monitor is currently in use. - pub const PRINT_MONITOR_IN_USE: Win32Error = Win32Error(3008); - /// The requested operation is not allowed when there are jobs queued to the printer. - pub const PRINTER_HAS_JOBS_QUEUED: Win32Error = Win32Error(3009); - /// The requested operation is successful. - /// Changes will not be effective until the system is rebooted. - pub const SUCCESS_REBOOT_REQUIRED: Win32Error = Win32Error(3010); - /// The requested operation is successful. - /// Changes will not be effective until the service is restarted. - pub const SUCCESS_RESTART_REQUIRED: Win32Error = Win32Error(3011); - /// No printers were found. - pub const PRINTER_NOT_FOUND: Win32Error = Win32Error(3012); - /// The printer driver is known to be unreliable. - pub const PRINTER_DRIVER_WARNED: Win32Error = Win32Error(3013); - /// The printer driver is known to harm the system. - pub const PRINTER_DRIVER_BLOCKED: Win32Error = Win32Error(3014); - /// The specified printer driver package is currently in use. - pub const PRINTER_DRIVER_PACKAGE_IN_USE: Win32Error = Win32Error(3015); - /// Unable to find a core driver package that is required by the printer driver package. - pub const CORE_DRIVER_PACKAGE_NOT_FOUND: Win32Error = Win32Error(3016); - /// The requested operation failed. - /// A system reboot is required to roll back changes made. - pub const FAIL_REBOOT_REQUIRED: Win32Error = Win32Error(3017); - /// The requested operation failed. - /// A system reboot has been initiated to roll back changes made. - pub const FAIL_REBOOT_INITIATED: Win32Error = Win32Error(3018); - /// The specified printer driver was not found on the system and needs to be downloaded. - pub const PRINTER_DRIVER_DOWNLOAD_NEEDED: Win32Error = Win32Error(3019); - /// The requested print job has failed to print. - /// A print system update requires the job to be resubmitted. - pub const PRINT_JOB_RESTART_REQUIRED: Win32Error = Win32Error(3020); - /// The printer driver does not contain a valid manifest, or contains too many manifests. - pub const INVALID_PRINTER_DRIVER_MANIFEST: Win32Error = Win32Error(3021); - /// The specified printer cannot be shared. - pub const PRINTER_NOT_SHAREABLE: Win32Error = Win32Error(3022); - /// The operation was paused. - pub const REQUEST_PAUSED: Win32Error = Win32Error(3050); - /// Reissue the given operation as a cached IO operation. - pub const IO_REISSUE_AS_CACHED: Win32Error = Win32Error(3950); - - /// An application attempts to use an event object, but the specified handle is not valid. - pub const WSA_INVALID_HANDLE: Win32Error = Win32Error(6); - - /// An application used a Windows Sockets function that directly maps to a Windows function. The Windows function is indicating a lack of required memory resources. - pub const WSA_NOT_ENOUGH_MEMORY: Win32Error = Win32Error(8); - - /// An application used a Windows Sockets function which directly maps to a Windows function. The Windows function is indicating a problem with one or more parameters. - pub const WSA_INVALID_PARAMETER: Win32Error = Win32Error(87); - - /// An overlapped operation was canceled due to the closure of the socket, or the execution of the SIO_FLUSH command in WSAIoctl. - pub const WSA_OPERATION_ABORTED: Win32Error = Win32Error(995); - - /// The application has tried to determine the status of an overlapped operation which is not yet completed. Applications that use WSAGetOverlappedResult (with the fWait flag set to FALSE) in a polling mode to determine when an overlapped operation has completed, get this error code until the operation is complete. - pub const WSA_IO_INCOMPLETE: Win32Error = Win32Error(996); - - /// The application has initiated an overlapped operation that cannot be completed immediately. A completion indication will be given later when the operation has been completed. - pub const WSA_IO_PENDING: Win32Error = Win32Error(997); - - /// A blocking operation was interrupted by a call to WSACancelBlockingCall. - pub const WSAEINTR: Win32Error = Win32Error(10004); - - /// The file handle supplied is not valid. - pub const WSAEBADF: Win32Error = Win32Error(10009); - - /// An attempt was made to access a socket in a way forbidden by its access permissions. An example is using a broadcast address for sendto without broadcast permission being set using setsockopt(SO_BROADCAST). - /// Another possible reason for the WSAEACCES error is that when the bind function is called (on Windows NT 4.0 with SP4 and later), another application, service, or kernel mode driver is bound to the same address with exclusive access. Such exclusive access is a new feature of Windows NT 4.0 with SP4 and later, and is implemented by using the SO_EXCLUSIVEADDRUSE option. - pub const WSAEACCES: Win32Error = Win32Error(10013); - - /// The system detected an invalid pointer address in attempting to use a pointer argument of a call. This error occurs if an application passes an invalid pointer value, or if the length of the buffer is too small. For instance, if the length of an argument, which is a sockaddr structure, is smaller than the sizeof(sockaddr). - pub const WSAEFAULT: Win32Error = Win32Error(10014); - - /// Some invalid argument was supplied (for example, specifying an invalid level to the setsockopt function). In some instances, it also refers to the current state of the socket—for instance, calling accept on a socket that is not listening. - pub const WSAEINVAL: Win32Error = Win32Error(10022); - - /// Too many open sockets. Each implementation may have a maximum number of socket handles available, either globally, per process, or per thread. - pub const WSAEMFILE: Win32Error = Win32Error(10024); - - /// This error is returned from operations on nonblocking sockets that cannot be completed immediately, for example recv when no data is queued to be read from the socket. It is a nonfatal error, and the operation should be retried later. It is normal for WSAEWOULDBLOCK to be reported as the result from calling connect on a nonblocking SOCK_STREAM socket, since some time must elapse for the connection to be established. - pub const WSAEWOULDBLOCK: Win32Error = Win32Error(10035); - - /// A blocking operation is currently executing. Windows Sockets only allows a single blocking operation—per- task or thread—to be outstanding, and if any other function call is made (whether or not it references that or any other socket) the function fails with the WSAEINPROGRESS error. - pub const WSAEINPROGRESS: Win32Error = Win32Error(10036); - - /// An operation was attempted on a nonblocking socket with an operation already in progress—that is, calling connect a second time on a nonblocking socket that is already connecting, or canceling an asynchronous request (WSAAsyncGetXbyY) that has already been canceled or completed. - pub const WSAEALREADY: Win32Error = Win32Error(10037); - - /// An operation was attempted on something that is not a socket. Either the socket handle parameter did not reference a valid socket, or for select, a member of an fd_set was not valid. - pub const WSAENOTSOCK: Win32Error = Win32Error(10038); - - /// A required address was omitted from an operation on a socket. For example, this error is returned if sendto is called with the remote address of ADDR_ANY. - pub const WSAEDESTADDRREQ: Win32Error = Win32Error(10039); - - /// A message sent on a datagram socket was larger than the internal message buffer or some other network limit, or the buffer used to receive a datagram was smaller than the datagram itself. - pub const WSAEMSGSIZE: Win32Error = Win32Error(10040); - - /// A protocol was specified in the socket function call that does not support the semantics of the socket type requested. For example, the ARPA Internet UDP protocol cannot be specified with a socket type of SOCK_STREAM. - pub const WSAEPROTOTYPE: Win32Error = Win32Error(10041); - - /// An unknown, invalid or unsupported option or level was specified in a getsockopt or setsockopt call. - pub const WSAENOPROTOOPT: Win32Error = Win32Error(10042); - - /// The requested protocol has not been configured into the system, or no implementation for it exists. For example, a socket call requests a SOCK_DGRAM socket, but specifies a stream protocol. - pub const WSAEPROTONOSUPPORT: Win32Error = Win32Error(10043); - - /// The support for the specified socket type does not exist in this address family. For example, the optional type SOCK_RAW might be selected in a socket call, and the implementation does not support SOCK_RAW sockets at all. - pub const WSAESOCKTNOSUPPORT: Win32Error = Win32Error(10044); - - /// The attempted operation is not supported for the type of object referenced. Usually this occurs when a socket descriptor to a socket that cannot support this operation is trying to accept a connection on a datagram socket. - pub const WSAEOPNOTSUPP: Win32Error = Win32Error(10045); - - /// The protocol family has not been configured into the system or no implementation for it exists. This message has a slightly different meaning from WSAEAFNOSUPPORT. However, it is interchangeable in most cases, and all Windows Sockets functions that return one of these messages also specify WSAEAFNOSUPPORT. - pub const WSAEPFNOSUPPORT: Win32Error = Win32Error(10046); - - /// An address incompatible with the requested protocol was used. All sockets are created with an associated address family (that is, AF_INET for Internet Protocols) and a generic protocol type (that is, SOCK_STREAM). This error is returned if an incorrect protocol is explicitly requested in the socket call, or if an address of the wrong family is used for a socket, for example, in sendto. - pub const WSAEAFNOSUPPORT: Win32Error = Win32Error(10047); - - /// Typically, only one usage of each socket address (protocol/IP address/port) is permitted. This error occurs if an application attempts to bind a socket to an IP address/port that has already been used for an existing socket, or a socket that was not closed properly, or one that is still in the process of closing. For server applications that need to bind multiple sockets to the same port number, consider using setsockopt (SO_REUSEADDR). Client applications usually need not call bind at all—connect chooses an unused port automatically. When bind is called with a wildcard address (involving ADDR_ANY), a WSAEADDRINUSE error could be delayed until the specific address is committed. This could happen with a call to another function later, including connect, listen, WSAConnect, or WSAJoinLeaf. - pub const WSAEADDRINUSE: Win32Error = Win32Error(10048); - - /// The requested address is not valid in its context. This normally results from an attempt to bind to an address that is not valid for the local computer. This can also result from connect, sendto, WSAConnect, WSAJoinLeaf, or WSASendTo when the remote address or port is not valid for a remote computer (for example, address or port 0). - pub const WSAEADDRNOTAVAIL: Win32Error = Win32Error(10049); - - /// A socket operation encountered a dead network. This could indicate a serious failure of the network system (that is, the protocol stack that the Windows Sockets DLL runs over), the network interface, or the local network itself. - pub const WSAENETDOWN: Win32Error = Win32Error(10050); - - /// A socket operation was attempted to an unreachable network. This usually means the local software knows no route to reach the remote host. - pub const WSAENETUNREACH: Win32Error = Win32Error(10051); - - /// The connection has been broken due to keep-alive activity detecting a failure while the operation was in progress. It can also be returned by setsockopt if an attempt is made to set SO_KEEPALIVE on a connection that has already failed. - pub const WSAENETRESET: Win32Error = Win32Error(10052); - - /// An established connection was aborted by the software in your host computer, possibly due to a data transmission time-out or protocol error. - pub const WSAECONNABORTED: Win32Error = Win32Error(10053); - - /// An existing connection was forcibly closed by the remote host. This normally results if the peer application on the remote host is suddenly stopped, the host is rebooted, the host or remote network interface is disabled, or the remote host uses a hard close (see setsockopt for more information on the SO_LINGER option on the remote socket). This error may also result if a connection was broken due to keep-alive activity detecting a failure while one or more operations are in progress. Operations that were in progress fail with WSAENETRESET. Subsequent operations fail with WSAECONNRESET. - pub const WSAECONNRESET: Win32Error = Win32Error(10054); - - /// An operation on a socket could not be performed because the system lacked sufficient buffer space or because a queue was full. - pub const WSAENOBUFS: Win32Error = Win32Error(10055); - - /// A connect request was made on an already-connected socket. Some implementations also return this error if sendto is called on a connected SOCK_DGRAM socket (for SOCK_STREAM sockets, the to parameter in sendto is ignored) although other implementations treat this as a legal occurrence. - pub const WSAEISCONN: Win32Error = Win32Error(10056); - - /// A request to send or receive data was disallowed because the socket is not connected and (when sending on a datagram socket using sendto) no address was supplied. Any other type of operation might also return this error—for example, setsockopt setting SO_KEEPALIVE if the connection has been reset. - pub const WSAENOTCONN: Win32Error = Win32Error(10057); - - /// A request to send or receive data was disallowed because the socket had already been shut down in that direction with a previous shutdown call. By calling shutdown a partial close of a socket is requested, which is a signal that sending or receiving, or both have been discontinued. - pub const WSAESHUTDOWN: Win32Error = Win32Error(10058); - - /// Too many references to some kernel object. - pub const WSAETOOMANYREFS: Win32Error = Win32Error(10059); - - /// A connection attempt failed because the connected party did not properly respond after a period of time, or the established connection failed because the connected host has failed to respond. - pub const WSAETIMEDOUT: Win32Error = Win32Error(10060); - - /// No connection could be made because the target computer actively refused it. This usually results from trying to connect to a service that is inactive on the foreign host—that is, one with no server application running. - pub const WSAECONNREFUSED: Win32Error = Win32Error(10061); - - /// Cannot translate a name. - pub const WSAELOOP: Win32Error = Win32Error(10062); - - /// A name component or a name was too long. - pub const WSAENAMETOOLONG: Win32Error = Win32Error(10063); - - /// A socket operation failed because the destination host is down. A socket operation encountered a dead host. Networking activity on the local host has not been initiated. These conditions are more likely to be indicated by the error WSAETIMEDOUT. - pub const WSAEHOSTDOWN: Win32Error = Win32Error(10064); - - /// A socket operation was attempted to an unreachable host. See WSAENETUNREACH. - pub const WSAEHOSTUNREACH: Win32Error = Win32Error(10065); - - /// Cannot remove a directory that is not empty. - pub const WSAENOTEMPTY: Win32Error = Win32Error(10066); - - /// A Windows Sockets implementation may have a limit on the number of applications that can use it simultaneously. WSAStartup may fail with this error if the limit has been reached. - pub const WSAEPROCLIM: Win32Error = Win32Error(10067); - - /// Ran out of user quota. - pub const WSAEUSERS: Win32Error = Win32Error(10068); - - /// Ran out of disk quota. - pub const WSAEDQUOT: Win32Error = Win32Error(10069); - - /// The file handle reference is no longer available. - pub const WSAESTALE: Win32Error = Win32Error(10070); - - /// The item is not available locally. - pub const WSAEREMOTE: Win32Error = Win32Error(10071); - - /// This error is returned by WSAStartup if the Windows Sockets implementation cannot function at this time because the underlying system it uses to provide network services is currently unavailable. Users should check: - pub const WSASYSNOTREADY: Win32Error = Win32Error(10091); - - /// The current Windows Sockets implementation does not support the Windows Sockets specification version requested by the application. Check that no old Windows Sockets DLL files are being accessed. - pub const WSAVERNOTSUPPORTED: Win32Error = Win32Error(10092); - - /// Either the application has not called WSAStartup or WSAStartup failed. The application may be accessing a socket that the current active task does not own (that is, trying to share a socket between tasks), or WSACleanup has been called too many times. - pub const WSANOTINITIALISED: Win32Error = Win32Error(10093); - - /// Returned by WSARecv and WSARecvFrom to indicate that the remote party has initiated a graceful shutdown sequence. - pub const WSAEDISCON: Win32Error = Win32Error(10101); - - /// No more results can be returned by the WSALookupServiceNext function. - pub const WSAENOMORE: Win32Error = Win32Error(10102); - - /// A call to the WSALookupServiceEnd function was made while this call was still processing. The call has been canceled. - pub const WSAECANCELLED: Win32Error = Win32Error(10103); - - /// The service provider procedure call table is invalid. A service provider returned a bogus procedure table to Ws2_32.dll. This is usually caused by one or more of the function pointers being NULL. - pub const WSAEINVALIDPROCTABLE: Win32Error = Win32Error(10104); - - /// The requested service provider is invalid. This error is returned by the WSCGetProviderInfo and WSCGetProviderInfo32 functions if the protocol entry specified could not be found. This error is also returned if the service provider returned a version number other than 2.0. - pub const WSAEINVALIDPROVIDER: Win32Error = Win32Error(10105); - - /// The requested service provider could not be loaded or initialized. This error is returned if either a service provider's DLL could not be loaded (LoadLibrary failed) or the provider's WSPStartup or NSPStartup function failed. - pub const WSAEPROVIDERFAILEDINIT: Win32Error = Win32Error(10106); - - /// A system call that should never fail has failed. This is a generic error code, returned under various conditions. - /// Returned when a system call that should never fail does fail. For example, if a call to WaitForMultipleEvents fails or one of the registry functions fails trying to manipulate the protocol/namespace catalogs. - /// Returned when a provider does not return SUCCESS and does not provide an extended error code. Can indicate a service provider implementation error. - pub const WSASYSCALLFAILURE: Win32Error = Win32Error(10107); - - /// No such service is known. The service cannot be found in the specified name space. - pub const WSASERVICE_NOT_FOUND: Win32Error = Win32Error(10108); - - /// The specified class was not found. - pub const WSATYPE_NOT_FOUND: Win32Error = Win32Error(10109); - - /// No more results can be returned by the WSALookupServiceNext function. - pub const WSA_E_NO_MORE: Win32Error = Win32Error(10110); - - /// A call to the WSALookupServiceEnd function was made while this call was still processing. The call has been canceled. - pub const WSA_E_CANCELLED: Win32Error = Win32Error(10111); - - /// A database query failed because it was actively refused. - pub const WSAEREFUSED: Win32Error = Win32Error(10112); - - /// No such host is known. The name is not an official host name or alias, or it cannot be found in the database(s) being queried. This error may also be returned for protocol and service queries, and means that the specified name could not be found in the relevant database. - pub const WSAHOST_NOT_FOUND: Win32Error = Win32Error(11001); - - /// This is usually a temporary error during host name resolution and means that the local server did not receive a response from an authoritative server. A retry at some time later may be successful. - pub const WSATRY_AGAIN: Win32Error = Win32Error(11002); - - /// This indicates that some sort of nonrecoverable error occurred during a database lookup. This may be because the database files (for example, BSD-compatible HOSTS, SERVICES, or PROTOCOLS files) could not be found, or a DNS request was returned by the server with a severe error. - pub const WSANO_RECOVERY: Win32Error = Win32Error(11003); - - /// The requested name is valid and was found in the database, but it does not have the correct associated data being resolved for. The usual example for this is a host name-to-address translation attempt (using gethostbyname or WSAAsyncGetHostByName) which uses the DNS (Domain Name Server). An MX record is returned but no A record—indicating the host itself exists, but is not directly reachable. - pub const WSANO_DATA: Win32Error = Win32Error(11004); - - /// At least one QoS reserve has arrived. - pub const WSA_QOS_RECEIVERS: Win32Error = Win32Error(11005); - - /// At least one QoS send path has arrived. - pub const WSA_QOS_SENDERS: Win32Error = Win32Error(11006); - - /// There are no QoS senders. - pub const WSA_QOS_NO_SENDERS: Win32Error = Win32Error(11007); - - /// There are no QoS receivers. - pub const WSA_QOS_NO_RECEIVERS: Win32Error = Win32Error(11008); - - /// The QoS reserve request has been confirmed. - pub const WSA_QOS_REQUEST_CONFIRMED: Win32Error = Win32Error(11009); - - /// A QoS error occurred due to lack of resources. - pub const WSA_QOS_ADMISSION_FAILURE: Win32Error = Win32Error(11010); - - /// The QoS request was rejected because the policy system couldn't allocate the requested resource within the existing policy. - pub const WSA_QOS_POLICY_FAILURE: Win32Error = Win32Error(11011); - - /// An unknown or conflicting QoS style was encountered. - pub const WSA_QOS_BAD_STYLE: Win32Error = Win32Error(11012); - - /// A problem was encountered with some part of the filterspec or the provider-specific buffer in general. - pub const WSA_QOS_BAD_OBJECT: Win32Error = Win32Error(11013); - - /// An error with the underlying traffic control (TC) API as the generic QoS request was converted for local enforcement by the TC API. This could be due to an out of memory error or to an internal QoS provider error. - pub const WSA_QOS_TRAFFIC_CTRL_ERROR: Win32Error = Win32Error(11014); - - /// A general QoS error. - pub const WSA_QOS_GENERIC_ERROR: Win32Error = Win32Error(11015); - - /// An invalid or unrecognized service type was found in the QoS flowspec. - pub const WSA_QOS_ESERVICETYPE: Win32Error = Win32Error(11016); - - /// An invalid or inconsistent flowspec was found in the QOS structure. - pub const WSA_QOS_EFLOWSPEC: Win32Error = Win32Error(11017); - - /// An invalid QoS provider-specific buffer. - pub const WSA_QOS_EPROVSPECBUF: Win32Error = Win32Error(11018); - - /// An invalid QoS filter style was used. - pub const WSA_QOS_EFILTERSTYLE: Win32Error = Win32Error(11019); - - /// An invalid QoS filter type was used. - pub const WSA_QOS_EFILTERTYPE: Win32Error = Win32Error(11020); - - /// An incorrect number of QoS FILTERSPECs were specified in the FLOWDESCRIPTOR. - pub const WSA_QOS_EFILTERCOUNT: Win32Error = Win32Error(11021); - - /// An object with an invalid ObjectLength field was specified in the QoS provider-specific buffer. - pub const WSA_QOS_EOBJLENGTH: Win32Error = Win32Error(11022); - - /// An incorrect number of flow descriptors was specified in the QoS structure. - pub const WSA_QOS_EFLOWCOUNT: Win32Error = Win32Error(11023); - - /// An unrecognized object was found in the QoS provider-specific buffer. - pub const WSA_QOS_EUNKOWNPSOBJ: Win32Error = Win32Error(11024); - - /// An invalid policy object was found in the QoS provider-specific buffer. - pub const WSA_QOS_EPOLICYOBJ: Win32Error = Win32Error(11025); - - /// An invalid QoS flow descriptor was found in the flow descriptor list. - pub const WSA_QOS_EFLOWDESC: Win32Error = Win32Error(11026); - - /// An invalid or inconsistent flowspec was found in the QoS provider-specific buffer. - pub const WSA_QOS_EPSFLOWSPEC: Win32Error = Win32Error(11027); - - /// An invalid FILTERSPEC was found in the QoS provider-specific buffer. - pub const WSA_QOS_EPSFILTERSPEC: Win32Error = Win32Error(11028); - - /// An invalid shape discard mode object was found in the QoS provider-specific buffer. - pub const WSA_QOS_ESDMODEOBJ: Win32Error = Win32Error(11029); - - /// An invalid shaping rate object was found in the QoS provider-specific buffer. - pub const WSA_QOS_ESHAPERATEOBJ: Win32Error = Win32Error(11030); - - /// A reserved policy element was found in the QoS provider-specific buffer. - pub const WSA_QOS_RESERVED_PETYPE: Win32Error = Win32Error(11031); - } -} // mod _win32error_full_table - pub use bun_libuv_sys as libuv; pub use bun_errno::translate_uv_error_to_e; @@ -3485,117 +675,6 @@ pub fn user_unique_id() -> u32 { bun_wyhash::hash32(bytemuck::cast_slice::(name)) } -pub fn win_sock_error_to_zig_error( - err: win32::ws2_32::WinsockError, -) -> Result<(), bun_core::Error> { - use win32::ws2_32::WinsockError as W; - let tag = match err { - W::WSA_INVALID_HANDLE => "WSA_INVALID_HANDLE", - W::WSA_NOT_ENOUGH_MEMORY => "WSA_NOT_ENOUGH_MEMORY", - W::WSA_INVALID_PARAMETER => "WSA_INVALID_PARAMETER", - W::WSA_OPERATION_ABORTED => "WSA_OPERATION_ABORTED", - W::WSA_IO_INCOMPLETE => "WSA_IO_INCOMPLETE", - W::WSA_IO_PENDING => "WSA_IO_PENDING", - W::WSAEINTR => "WSAEINTR", - W::WSAEBADF => "WSAEBADF", - W::WSAEACCES => "WSAEACCES", - W::WSAEFAULT => "WSAEFAULT", - W::WSAEINVAL => "WSAEINVAL", - W::WSAEMFILE => "WSAEMFILE", - W::WSAEWOULDBLOCK => "WSAEWOULDBLOCK", - W::WSAEINPROGRESS => "WSAEINPROGRESS", - W::WSAEALREADY => "WSAEALREADY", - W::WSAENOTSOCK => "WSAENOTSOCK", - W::WSAEDESTADDRREQ => "WSAEDESTADDRREQ", - W::WSAEMSGSIZE => "WSAEMSGSIZE", - W::WSAEPROTOTYPE => "WSAEPROTOTYPE", - W::WSAENOPROTOOPT => "WSAENOPROTOOPT", - W::WSAEPROTONOSUPPORT => "WSAEPROTONOSUPPORT", - W::WSAESOCKTNOSUPPORT => "WSAESOCKTNOSUPPORT", - W::WSAEOPNOTSUPP => "WSAEOPNOTSUPP", - W::WSAEPFNOSUPPORT => "WSAEPFNOSUPPORT", - W::WSAEAFNOSUPPORT => "WSAEAFNOSUPPORT", - W::WSAEADDRINUSE => "WSAEADDRINUSE", - W::WSAEADDRNOTAVAIL => "WSAEADDRNOTAVAIL", - W::WSAENETDOWN => "WSAENETDOWN", - W::WSAENETUNREACH => "WSAENETUNREACH", - W::WSAENETRESET => "WSAENETRESET", - W::WSAECONNABORTED => "WSAECONNABORTED", - W::WSAECONNRESET => "WSAECONNRESET", - W::WSAENOBUFS => "WSAENOBUFS", - W::WSAEISCONN => "WSAEISCONN", - W::WSAENOTCONN => "WSAENOTCONN", - W::WSAESHUTDOWN => "WSAESHUTDOWN", - W::WSAETOOMANYREFS => "WSAETOOMANYREFS", - W::WSAETIMEDOUT => "WSAETIMEDOUT", - W::WSAECONNREFUSED => "WSAECONNREFUSED", - W::WSAELOOP => "WSAELOOP", - W::WSAENAMETOOLONG => "WSAENAMETOOLONG", - W::WSAEHOSTDOWN => "WSAEHOSTDOWN", - W::WSAEHOSTUNREACH => "WSAEHOSTUNREACH", - W::WSAENOTEMPTY => "WSAENOTEMPTY", - W::WSAEPROCLIM => "WSAEPROCLIM", - W::WSAEUSERS => "WSAEUSERS", - W::WSAEDQUOT => "WSAEDQUOT", - W::WSAESTALE => "WSAESTALE", - W::WSAEREMOTE => "WSAEREMOTE", - W::WSASYSNOTREADY => "WSASYSNOTREADY", - W::WSAVERNOTSUPPORTED => "WSAVERNOTSUPPORTED", - W::WSANOTINITIALISED => "WSANOTINITIALISED", - W::WSAEDISCON => "WSAEDISCON", - W::WSAENOMORE => "WSAENOMORE", - W::WSAECANCELLED => "WSAECANCELLED", - W::WSAEINVALIDPROCTABLE => "WSAEINVALIDPROCTABLE", - W::WSAEINVALIDPROVIDER => "WSAEINVALIDPROVIDER", - W::WSAEPROVIDERFAILEDINIT => "WSAEPROVIDERFAILEDINIT", - W::WSASYSCALLFAILURE => "WSASYSCALLFAILURE", - W::WSASERVICE_NOT_FOUND => "WSASERVICE_NOT_FOUND", - W::WSATYPE_NOT_FOUND => "WSATYPE_NOT_FOUND", - W::WSA_E_NO_MORE => "WSA_E_NO_MORE", - W::WSA_E_CANCELLED => "WSA_E_CANCELLED", - W::WSAEREFUSED => "WSAEREFUSED", - W::WSAHOST_NOT_FOUND => "WSAHOST_NOT_FOUND", - W::WSATRY_AGAIN => "WSATRY_AGAIN", - W::WSANO_RECOVERY => "WSANO_RECOVERY", - W::WSANO_DATA => "WSANO_DATA", - W::WSA_QOS_RECEIVERS => "WSA_QOS_RECEIVERS", - W::WSA_QOS_SENDERS => "WSA_QOS_SENDERS", - W::WSA_QOS_NO_SENDERS => "WSA_QOS_NO_SENDERS", - W::WSA_QOS_NO_RECEIVERS => "WSA_QOS_NO_RECEIVERS", - W::WSA_QOS_REQUEST_CONFIRMED => "WSA_QOS_REQUEST_CONFIRMED", - W::WSA_QOS_ADMISSION_FAILURE => "WSA_QOS_ADMISSION_FAILURE", - W::WSA_QOS_POLICY_FAILURE => "WSA_QOS_POLICY_FAILURE", - W::WSA_QOS_BAD_STYLE => "WSA_QOS_BAD_STYLE", - W::WSA_QOS_BAD_OBJECT => "WSA_QOS_BAD_OBJECT", - W::WSA_QOS_TRAFFIC_CTRL_ERROR => "WSA_QOS_TRAFFIC_CTRL_ERROR", - W::WSA_QOS_GENERIC_ERROR => "WSA_QOS_GENERIC_ERROR", - W::WSA_QOS_ESERVICETYPE => "WSA_QOS_ESERVICETYPE", - W::WSA_QOS_EFLOWSPEC => "WSA_QOS_EFLOWSPEC", - W::WSA_QOS_EPROVSPECBUF => "WSA_QOS_EPROVSPECBUF", - W::WSA_QOS_EFILTERSTYLE => "WSA_QOS_EFILTERSTYLE", - W::WSA_QOS_EFILTERTYPE => "WSA_QOS_EFILTERTYPE", - W::WSA_QOS_EFILTERCOUNT => "WSA_QOS_EFILTERCOUNT", - W::WSA_QOS_EOBJLENGTH => "WSA_QOS_EOBJLENGTH", - W::WSA_QOS_EFLOWCOUNT => "WSA_QOS_EFLOWCOUNT", - W::WSA_QOS_EUNKOWNPSOBJ => "WSA_QOS_EUNKOWNPSOBJ", - W::WSA_QOS_EPOLICYOBJ => "WSA_QOS_EPOLICYOBJ", - W::WSA_QOS_EFLOWDESC => "WSA_QOS_EFLOWDESC", - W::WSA_QOS_EPSFLOWSPEC => "WSA_QOS_EPSFLOWSPEC", - W::WSA_QOS_EPSFILTERSPEC => "WSA_QOS_EPSFILTERSPEC", - W::WSA_QOS_ESDMODEOBJ => "WSA_QOS_ESDMODEOBJ", - W::WSA_QOS_ESHAPERATEOBJ => "WSA_QOS_ESHAPERATEOBJ", - W::WSA_QOS_RESERVED_PETYPE => "WSA_QOS_RESERVED_PETYPE", - t => { - if t.0 != 0 { - #[cfg(debug_assertions)] - bun_core::debug_warn!("Unknown WinSockError: {}", t.0); - } - return Ok(()); - } - }; - Err(bun_core::Error::intern(tag)) -} - pub fn WSAGetLastError() -> Option { // Returns `Option` because all callers consume `E`. // `WSAGetLastError()` is documented to return non-negative values, so the 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 075fa6224042..6e20f6692c94 100644 --- a/src/threading/channel.rs +++ b/src/threading/channel.rs @@ -60,14 +60,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 { @@ -91,11 +83,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 856e9e2013c4..0b8fbedfa1bc 100644 --- a/src/url/Cargo.toml +++ b/src/url/Cargo.toml @@ -20,6 +20,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 12c404b1cb3a..ac22636ad276 100644 --- a/src/url/lib.rs +++ b/src/url/lib.rs @@ -42,11 +42,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 @@ -122,12 +123,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 { @@ -185,6 +196,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/src/windows_sys/externs.rs b/src/windows_sys/externs.rs index bba668861d68..13445862d164 100644 --- a/src/windows_sys/externs.rs +++ b/src/windows_sys/externs.rs @@ -917,114 +917,6 @@ pub mod ws2_32 { pub s6_addr: [u8; 16], } - /// Winsock error codes — `WSAE*` (`WSABASEERR` = 10000). - /// Newtype so `bun_sys::windows::win_sock_error_to_zig_error` can `match` on - /// associated consts. Values from `winsock2.h`. - #[repr(transparent)] - #[derive(Copy, Clone, PartialEq, Eq, Debug)] - pub struct WinsockError(pub u16); - impl WinsockError { - #[inline] - pub const fn raw(self) -> u16 { - self.0 - } - pub const WSA_INVALID_HANDLE: Self = Self(6); - pub const WSA_NOT_ENOUGH_MEMORY: Self = Self(8); - pub const WSA_INVALID_PARAMETER: Self = Self(87); - pub const WSA_OPERATION_ABORTED: Self = Self(995); - pub const WSA_IO_INCOMPLETE: Self = Self(996); - pub const WSA_IO_PENDING: Self = Self(997); - pub const WSAEINTR: Self = Self(10004); - pub const WSAEBADF: Self = Self(10009); - pub const WSAEACCES: Self = Self(10013); - pub const WSAEFAULT: Self = Self(10014); - pub const WSAEINVAL: Self = Self(10022); - pub const WSAEMFILE: Self = Self(10024); - pub const WSAEWOULDBLOCK: Self = Self(10035); - pub const WSAEINPROGRESS: Self = Self(10036); - pub const WSAEALREADY: Self = Self(10037); - pub const WSAENOTSOCK: Self = Self(10038); - pub const WSAEDESTADDRREQ: Self = Self(10039); - pub const WSAEMSGSIZE: Self = Self(10040); - pub const WSAEPROTOTYPE: Self = Self(10041); - pub const WSAENOPROTOOPT: Self = Self(10042); - pub const WSAEPROTONOSUPPORT: Self = Self(10043); - pub const WSAESOCKTNOSUPPORT: Self = Self(10044); - pub const WSAEOPNOTSUPP: Self = Self(10045); - pub const WSAEPFNOSUPPORT: Self = Self(10046); - pub const WSAEAFNOSUPPORT: Self = Self(10047); - pub const WSAEADDRINUSE: Self = Self(10048); - pub const WSAEADDRNOTAVAIL: Self = Self(10049); - pub const WSAENETDOWN: Self = Self(10050); - pub const WSAENETUNREACH: Self = Self(10051); - pub const WSAENETRESET: Self = Self(10052); - pub const WSAECONNABORTED: Self = Self(10053); - pub const WSAECONNRESET: Self = Self(10054); - pub const WSAENOBUFS: Self = Self(10055); - pub const WSAEISCONN: Self = Self(10056); - pub const WSAENOTCONN: Self = Self(10057); - pub const WSAESHUTDOWN: Self = Self(10058); - pub const WSAETOOMANYREFS: Self = Self(10059); - pub const WSAETIMEDOUT: Self = Self(10060); - pub const WSAECONNREFUSED: Self = Self(10061); - pub const WSAELOOP: Self = Self(10062); - pub const WSAENAMETOOLONG: Self = Self(10063); - pub const WSAEHOSTDOWN: Self = Self(10064); - pub const WSAEHOSTUNREACH: Self = Self(10065); - pub const WSAENOTEMPTY: Self = Self(10066); - pub const WSAEPROCLIM: Self = Self(10067); - pub const WSAEUSERS: Self = Self(10068); - pub const WSAEDQUOT: Self = Self(10069); - pub const WSAESTALE: Self = Self(10070); - pub const WSAEREMOTE: Self = Self(10071); - pub const WSASYSNOTREADY: Self = Self(10091); - pub const WSAVERNOTSUPPORTED: Self = Self(10092); - pub const WSANOTINITIALISED: Self = Self(10093); - pub const WSAEDISCON: Self = Self(10101); - pub const WSAENOMORE: Self = Self(10102); - pub const WSAECANCELLED: Self = Self(10103); - pub const WSAEINVALIDPROCTABLE: Self = Self(10104); - pub const WSAEINVALIDPROVIDER: Self = Self(10105); - pub const WSAEPROVIDERFAILEDINIT: Self = Self(10106); - pub const WSASYSCALLFAILURE: Self = Self(10107); - pub const WSASERVICE_NOT_FOUND: Self = Self(10108); - pub const WSATYPE_NOT_FOUND: Self = Self(10109); - pub const WSA_E_NO_MORE: Self = Self(10110); - pub const WSA_E_CANCELLED: Self = Self(10111); - pub const WSAEREFUSED: Self = Self(10112); - pub const WSAHOST_NOT_FOUND: Self = Self(11001); - pub const WSATRY_AGAIN: Self = Self(11002); - pub const WSANO_RECOVERY: Self = Self(11003); - pub const WSANO_DATA: Self = Self(11004); - pub const WSA_QOS_RECEIVERS: Self = Self(11005); - pub const WSA_QOS_SENDERS: Self = Self(11006); - pub const WSA_QOS_NO_SENDERS: Self = Self(11007); - pub const WSA_QOS_NO_RECEIVERS: Self = Self(11008); - pub const WSA_QOS_REQUEST_CONFIRMED: Self = Self(11009); - pub const WSA_QOS_ADMISSION_FAILURE: Self = Self(11010); - pub const WSA_QOS_POLICY_FAILURE: Self = Self(11011); - pub const WSA_QOS_BAD_STYLE: Self = Self(11012); - pub const WSA_QOS_BAD_OBJECT: Self = Self(11013); - pub const WSA_QOS_TRAFFIC_CTRL_ERROR: Self = Self(11014); - pub const WSA_QOS_GENERIC_ERROR: Self = Self(11015); - pub const WSA_QOS_ESERVICETYPE: Self = Self(11016); - pub const WSA_QOS_EFLOWSPEC: Self = Self(11017); - pub const WSA_QOS_EPROVSPECBUF: Self = Self(11018); - pub const WSA_QOS_EFILTERSTYLE: Self = Self(11019); - pub const WSA_QOS_EFILTERTYPE: Self = Self(11020); - pub const WSA_QOS_EFILTERCOUNT: Self = Self(11021); - pub const WSA_QOS_EOBJLENGTH: Self = Self(11022); - pub const WSA_QOS_EFLOWCOUNT: Self = Self(11023); - pub const WSA_QOS_EUNKOWNPSOBJ: Self = Self(11024); - pub const WSA_QOS_EPOLICYOBJ: Self = Self(11025); - pub const WSA_QOS_EFLOWDESC: Self = Self(11026); - pub const WSA_QOS_EPSFLOWSPEC: Self = Self(11027); - pub const WSA_QOS_EPSFILTERSPEC: Self = Self(11028); - pub const WSA_QOS_ESDMODEOBJ: Self = Self(11029); - pub const WSA_QOS_ESHAPERATEOBJ: Self = Self(11030); - pub const WSA_QOS_RESERVED_PETYPE: Self = Self(11031); - } - #[link(name = "ws2_32")] unsafe extern "system" { /// Raw `WSAGetLastError`. The `Option` wrapper lives in `errno` diff --git a/src/zlib/Cargo.toml b/src/zlib/Cargo.toml index ab97722c782d..55b23bfbc03d 100644 --- a/src/zlib/Cargo.toml +++ b/src/zlib/Cargo.toml @@ -22,4 +22,3 @@ bun_alloc.workspace = true bun_collections.workspace = true bun_core.workspace = true bun_zlib_sys.workspace = true -bun_io.workspace = true diff --git a/src/zlib/lib.rs b/src/zlib/lib.rs index 76be3eb88710..5aeb7dd42854 100644 --- a/src/zlib/lib.rs +++ b/src/zlib/lib.rs @@ -142,187 +142,10 @@ unsafe extern "C" { pub fn crc32(crc: uLong, buf: *const Bytef, len: uInt) -> uLong; } -// `W: bun_io::Write` bound is applied on `read_all` (the only method that touches `context`). -pub struct ZlibReader<'a, W, const BUFFER_SIZE: usize> { - pub context: W, - pub input: &'a [u8], - pub buf: [u8; BUFFER_SIZE], - pub zlib: zStream_struct, - // allocator field dropped (global mimalloc) - pub state: ZlibReaderState, -} - pub use bun_core::compress::State; -pub type ZlibReaderState = State; pub type ZlibReaderArrayListState = State; pub type ZlibCompressorArrayListState = State; -impl<'a, W, const BUFFER_SIZE: usize> ZlibReader<'a, W, BUFFER_SIZE> { - pub fn end(&mut self) { - if self.state == ZlibReaderState::Inflating { - // SAFETY: zlib was initialized via inflateInit2_; safe to end. - unsafe { inflateEnd(&raw mut self.zlib) }; - self.state = ZlibReaderState::End; - } - } - - pub fn init(writer: W, input: &'a [u8]) -> Result, ZlibError> { - let mut zlib_reader = Box::new(Self { - context: writer, - input, - buf: [0u8; BUFFER_SIZE], - zlib: bun_core::ffi::zeroed(), - state: ZlibReaderState::Uninitialized, - }); - - zlib_reader.zlib = zStream_struct { - next_in: input.as_ptr(), - avail_in: u32::try_from(input.len()).expect("int cast"), - total_in: u32::try_from(input.len()).expect("int cast") as _, - - next_out: zlib_reader.buf.as_mut_ptr(), - avail_out: BUFFER_SIZE as uInt, - total_out: BUFFER_SIZE as _, - - err_msg: core::ptr::null(), - alloc_func: Some(zlib_mi_malloc), - free_func: Some(zlib_mi_free), - - internal_state: core::ptr::null_mut(), - user_data: (&raw mut *zlib_reader).cast::(), - - data_type: DataType::Unknown, - adler: 0, - reserved: 0, - }; - - // SAFETY: zlib_reader.zlib is fully initialized; version/size match the linked zlib. - match unsafe { - inflateInit2_( - &raw mut zlib_reader.zlib, - 15 + 32, - zlibVersion().cast::(), - size_of::() as c_int, - ) - } { - ReturnCode::Ok => Ok(zlib_reader), - ReturnCode::MemError => { - drop(zlib_reader); - Err(ZlibError::OutOfMemory) - } - ReturnCode::StreamError => { - drop(zlib_reader); - Err(ZlibError::InvalidArgument) - } - ReturnCode::VersionError => { - drop(zlib_reader); - Err(ZlibError::InvalidArgument) - } - _ => unreachable!(), - } - } - - pub fn error_message(&self) -> Option<&[u8]> { - if !self.zlib.err_msg.is_null() { - // SAFETY: err_msg is a NUL-terminated C string from zlib (static or stream-owned). - return Some( - unsafe { bun_core::ffi::cstr(self.zlib.err_msg.cast::()) }.to_bytes(), - ); - } - None - } - - pub fn read_all(&mut self, is_done: bool) -> Result<(), bun_core::Error> - where - W: bun_io::Write, - { - while self.state == ZlibReaderState::Uninitialized - || self.state == ZlibReaderState::Inflating - { - // Before the call of inflate(), the application should ensure - // that at least one of the actions is possible, by providing - // more input and/or consuming more output, and updating the - // next_* and avail_* values accordingly. If the caller of - // inflate() does not provide both available input and available - // output space, it is possible that there will be no progress - // made. The application can consume the uncompressed output - // when it wants, for example when the output buffer is full - // (avail_out == 0), or after each call of inflate(). If inflate - // returns Z_OK and with zero avail_out, it must be called again - // after making room in the output buffer because there might be - // more output pending. - - // - Decompress more input starting at next_in and update - // next_in and avail_in accordingly. If not all input can be - // processed (because there is not enough room in the output - // buffer), then next_in and avail_in are updated accordingly, - // and processing will resume at this point for the next call - // of inflate(). - - // - Generate more output starting at next_out and update - // next_out and avail_out accordingly. inflate() provides as - // much output as possible, until there is no more input data - // or no more space in the output buffer (see below about the - // flush parameter). - - if self.zlib.avail_out == 0 { - self.context.write_all(&self.buf)?; - self.zlib.avail_out = BUFFER_SIZE as uInt; - self.zlib.next_out = self.buf.as_mut_ptr(); - } - - // Try to inflate even if avail_in is 0, as this could be a valid empty gzip stream - // SAFETY: self.zlib was initialized via inflateInit2_. - let rc = unsafe { inflate(&raw mut self.zlib, FlushValue::NoFlush) }; - self.state = ZlibReaderState::Inflating; - - match rc { - ReturnCode::StreamEnd => { - self.state = ZlibReaderState::End; - let remainder = &self.buf[0..BUFFER_SIZE - self.zlib.avail_out as usize]; - self.context.write_all(remainder)?; - self.end(); - return Ok(()); - } - ReturnCode::MemError => { - self.state = ZlibReaderState::Error; - return Err(bun_core::err!("OutOfMemory")); - } - ReturnCode::BufError => { - // BufError with avail_in == 0 means we need more input data - if self.zlib.avail_in == 0 { - if is_done { - // Stream is truncated - we're at EOF but decoder needs more data - self.state = ZlibReaderState::Error; - return Err(bun_core::err!("ZlibError")); - } - // Not at EOF - we can retry with more data - return Err(bun_core::err!("ShortRead")); - } - self.state = ZlibReaderState::Error; - return Err(bun_core::err!("ZlibError")); - } - ReturnCode::StreamError - | ReturnCode::DataError - | ReturnCode::NeedDict - | ReturnCode::VersionError - | ReturnCode::ErrNo => { - self.state = ZlibReaderState::Error; - return Err(bun_core::err!("ZlibError")); - } - ReturnCode::Ok => {} - } - } - Ok(()) - } -} - -impl<'a, W, const BUFFER_SIZE: usize> Drop for ZlibReader<'a, W, BUFFER_SIZE> { - fn drop(&mut self) { - self.end(); - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, strum::IntoStaticStr)] pub enum ZlibError { OutOfMemory, @@ -335,7 +158,26 @@ bun_core::impl_tag_error!(ZlibError); bun_core::named_error_set!(ZlibError); -// zlib `alloc_func`/`free_func` thunks → mimalloc. Shared by `ZlibReader` and +/// Map an `inflateInit2_`/`deflateInit2_` return code to a `ZlibError`. +fn map_init_return_code(rc: ReturnCode) -> Result<(), ZlibError> { + match rc { + ReturnCode::Ok => Ok(()), + ReturnCode::MemError => Err(ZlibError::OutOfMemory), + ReturnCode::StreamError | ReturnCode::VersionError => Err(ZlibError::InvalidArgument), + _ => unreachable!(), + } +} + +/// Point the stream's output at freshly reserved tail capacity of `list`, +/// capping `avail_out` at `budget` bytes (`usize::MAX` = unbounded). +fn regrow_output_tail(zlib: &mut zStream_struct, list: &mut Vec, budget: usize) { + // SAFETY: zlib writes the tail; len is truncated to `total_out` before any read. + let (next_out, avail_out) = unsafe { list.reserve_expand_tail(budget.min(4096)) }; + zlib.next_out = next_out; + zlib.avail_out = avail_out.min(budget) as uInt; +} + +// zlib `alloc_func`/`free_func` thunks → mimalloc, used by // `ZlibCompressorArrayList`. Intentionally // `mi_malloc`, NOT `mi_calloc` (see `ZlibAllocator::alloc` for the zeroing // heap-breakdown variant used by `ZlibReaderArrayList`). @@ -431,29 +273,15 @@ impl<'a> ZlibReaderArrayList<'a> { }; // SAFETY: zlib_reader.zlib is fully initialized; version/size match the linked zlib. - match unsafe { + map_init_return_code(unsafe { inflateInit2_( &raw mut zlib_reader.zlib, options.window_bits, zlibVersion().cast::(), size_of::() as c_int, ) - } { - ReturnCode::Ok => Ok(zlib_reader), - ReturnCode::MemError => { - drop(zlib_reader); - Err(ZlibError::OutOfMemory) - } - ReturnCode::StreamError => { - drop(zlib_reader); - Err(ZlibError::InvalidArgument) - } - ReturnCode::VersionError => { - drop(zlib_reader); - Err(ZlibError::InvalidArgument) - } - _ => unreachable!(), - } + })?; + Ok(zlib_reader) } pub fn error_message(&self) -> Option<&[u8]> { @@ -507,14 +335,8 @@ impl<'a> ZlibReaderArrayList<'a> { self.state = ZlibReaderArrayListState::Error; return Err(ZlibError::ZlibError); } - // SAFETY: zlib writes the tail; len is truncated to `total_out` before any read. - let (next_out, avail_out) = unsafe { - self.list_ptr - .reserve_expand_tail(remaining_budget.min(4096)) - }; - self.zlib.next_out = next_out; // Clamp so a single inflate call cannot write past `max_output_size`. - self.zlib.avail_out = avail_out.min(remaining_budget) as uInt; + regrow_output_tail(&mut self.zlib, self.list_ptr, remaining_budget); } // Try to inflate even if avail_in is 0, as this could be a valid empty gzip stream @@ -930,7 +752,7 @@ impl<'a> ZlibCompressorArrayList<'a> { }; // SAFETY: zlib_reader.zlib is fully initialized; version/size match the linked zlib. - match unsafe { + map_init_return_code(unsafe { deflateInit2_( &raw mut zlib_reader.zlib, options.level, @@ -945,37 +767,22 @@ impl<'a> ZlibCompressorArrayList<'a> { zlibVersion().cast::(), size_of::() as c_int, ) - } { - ReturnCode::Ok => { - // SAFETY: zlib initialized; deflateBound returns upper bound on output. - let bound = unsafe { - deflateBound( - &raw mut zlib_reader.zlib, - uLong::try_from(input.len()).expect("int cast"), - ) - }; - // ensureTotalCapacityPrecise → reserve_exact - let need = (bound as usize).saturating_sub(zlib_reader.list_ptr.len()); - zlib_reader.list_ptr.reserve_exact(need); - zlib_reader.zlib.avail_out = zlib_reader.list_ptr.capacity() as uInt; - zlib_reader.zlib.next_out = zlib_reader.list_ptr.as_mut_ptr(); - - Ok(zlib_reader) - } - ReturnCode::MemError => { - drop(zlib_reader); - Err(ZlibError::OutOfMemory) - } - ReturnCode::StreamError => { - drop(zlib_reader); - Err(ZlibError::InvalidArgument) - } - ReturnCode::VersionError => { - drop(zlib_reader); - Err(ZlibError::InvalidArgument) - } - _ => unreachable!(), - } + })?; + + // SAFETY: zlib initialized; deflateBound returns upper bound on output. + let bound = unsafe { + deflateBound( + &raw mut zlib_reader.zlib, + uLong::try_from(input.len()).expect("int cast"), + ) + }; + // ensureTotalCapacityPrecise → reserve_exact + let need = (bound as usize).saturating_sub(zlib_reader.list_ptr.len()); + zlib_reader.list_ptr.reserve_exact(need); + zlib_reader.zlib.avail_out = zlib_reader.list_ptr.capacity() as uInt; + zlib_reader.zlib.next_out = zlib_reader.list_ptr.as_mut_ptr(); + + Ok(zlib_reader) } pub fn error_message(&self) -> Option<&[u8]> { @@ -1020,10 +827,7 @@ impl<'a> ZlibCompressorArrayList<'a> { // flush parameter). if self.zlib.avail_out == 0 { - // SAFETY: zlib writes the tail; len is truncated to `total_out` before any read. - let (next_out, avail_out) = unsafe { self.list_ptr.reserve_expand_tail(4096) }; - self.zlib.next_out = next_out; - self.zlib.avail_out = avail_out as uInt; + regrow_output_tail(&mut self.zlib, self.list_ptr, usize::MAX); } if self.zlib.avail_out == 0 { @@ -1076,14 +880,8 @@ impl<'a> Drop for ZlibCompressorArrayList<'a> { } } -// Re-export from bun_zlib_sys, platform-selected. +// Re-export from bun_zlib_sys. mod internal { - #[cfg(not(windows))] - pub(super) use bun_zlib_sys::posix::{DataType, zStream_struct}; - #[cfg(not(windows))] - pub use bun_zlib_sys::posix::{FlushValue, ReturnCode, z_stream, z_streamp}; - #[cfg(windows)] - pub(super) use bun_zlib_sys::win32::{DataType, zStream_struct}; - #[cfg(windows)] - pub use bun_zlib_sys::win32::{FlushValue, ReturnCode, z_stream, z_streamp}; + pub(super) use bun_zlib_sys::shared::{DataType, zStream_struct}; + pub use bun_zlib_sys::shared::{FlushValue, ReturnCode, z_stream, z_streamp}; } diff --git a/src/zlib_sys/lib.rs b/src/zlib_sys/lib.rs index 1ef905b360e6..bf0de1783cae 100644 --- a/src/zlib_sys/lib.rs +++ b/src/zlib_sys/lib.rs @@ -1,5 +1,3 @@ #![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)] #![warn(unused_must_use)] -pub mod posix; pub mod shared; -pub mod win32; diff --git a/src/zlib_sys/posix.rs b/src/zlib_sys/posix.rs deleted file mode 100644 index 57f4919febd1..000000000000 --- a/src/zlib_sys/posix.rs +++ /dev/null @@ -1,43 +0,0 @@ -#![allow(non_camel_case_types, non_snake_case)] - -use core::ffi::{c_char, c_int}; - -pub use crate::shared::{ - DataType, FlushValue, ReturnCode, alloc_func, free_func, struct_internal_state, z_alloc_fn, - z_free_fn, z_stream, z_streamp, zStream_struct, -}; - -unsafe extern "C" { - pub safe fn zlibVersion() -> *const c_char; - - pub fn deflateInit_( - strm: z_streamp, - level: c_int, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn inflateInit_(strm: z_streamp, version: *const c_char, stream_size: c_int) -> ReturnCode; - pub fn deflateInit2_( - strm: z_streamp, - level: c_int, - method: c_int, - windowBits: c_int, - memLevel: c_int, - strategy: c_int, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn inflateInit2_( - strm: z_streamp, - windowBits: c_int, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn inflateBackInit_( - strm: z_streamp, - windowBits: c_int, - window: *mut u8, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; -} diff --git a/src/zlib_sys/win32.rs b/src/zlib_sys/win32.rs deleted file mode 100644 index fe7423daf777..000000000000 --- a/src/zlib_sys/win32.rs +++ /dev/null @@ -1,310 +0,0 @@ -#![allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] - -use core::ffi::{c_char, c_int, c_long, c_uint, c_ulong, c_ushort, c_void}; - -pub use crate::shared::{ - Bytef, DataType, FlushValue, ReturnCode, alloc_func, free_func, gzFile, gzFile_s, - internal_state, struct_gzFile_s, struct_internal_state, struct_z_stream_s, uInt, uLong, uLongf, - voidpf, z_alloc_func, z_free_func, z_stream, z_stream_s, z_streamp, zStream_struct, -}; - -pub type rsize_t = usize; -pub type _ino_t = c_ushort; -pub type ino_t = _ino_t; -pub type _dev_t = c_uint; -pub type dev_t = _dev_t; -pub type _off_t = c_long; -pub type off_t = _off_t; -type z_size_t = usize; -type voidpc = *const c_void; -type voidp = *mut c_void; - -#[repr(C)] -pub struct struct_gz_header_s { - pub text: c_int, - pub time: uLong, - pub xflags: c_int, - pub os: c_int, - pub extra: *mut Bytef, - pub extra_len: uInt, - pub extra_max: uInt, - pub name: *mut Bytef, - pub name_max: uInt, - pub comment: *mut Bytef, - pub comm_max: uInt, - pub hcrc: c_int, - pub done: c_int, -} -pub type gz_header = struct_gz_header_s; -pub type gz_headerp = *mut gz_header; - -pub type in_func = Option c_uint>; -pub type out_func = Option ReturnCode>; - -unsafe extern "C" { - pub safe fn zlibVersion() -> *const c_char; - pub fn deflate(strm: z_streamp, flush: FlushValue) -> ReturnCode; - pub fn deflateEnd(strm: z_streamp) -> ReturnCode; - pub fn inflate(strm: z_streamp, flush: FlushValue) -> ReturnCode; - pub fn inflateEnd(strm: z_streamp) -> ReturnCode; - pub fn deflateSetDictionary( - strm: z_streamp, - dictionary: *const Bytef, - dictLength: uInt, - ) -> ReturnCode; - pub fn deflateGetDictionary( - strm: z_streamp, - dictionary: *mut Bytef, - dictLength: *mut uInt, - ) -> ReturnCode; - pub fn deflateCopy(dest: z_streamp, source: z_streamp) -> ReturnCode; - pub fn deflateReset(strm: z_streamp) -> ReturnCode; - pub fn deflateParams(strm: z_streamp, level: c_int, strategy: c_int) -> ReturnCode; - pub fn deflateTune( - strm: z_streamp, - good_length: c_int, - max_lazy: c_int, - nice_length: c_int, - max_chain: c_int, - ) -> ReturnCode; - pub fn deflateBound(strm: z_streamp, sourceLen: uLong) -> uLong; - pub fn deflatePending(strm: z_streamp, pending: *mut c_uint, bits: *mut c_int) -> ReturnCode; - pub fn deflatePrime(strm: z_streamp, bits: c_int, value: c_int) -> ReturnCode; - pub fn deflateSetHeader(strm: z_streamp, head: gz_headerp) -> ReturnCode; - pub fn inflateSetDictionary( - strm: z_streamp, - dictionary: *const Bytef, - dictLength: uInt, - ) -> ReturnCode; - pub fn inflateGetDictionary( - strm: z_streamp, - dictionary: *mut Bytef, - dictLength: *mut uInt, - ) -> ReturnCode; - pub fn inflateSync(strm: z_streamp) -> ReturnCode; - pub fn inflateCopy(dest: z_streamp, source: z_streamp) -> ReturnCode; - pub fn inflateReset(strm: z_streamp) -> ReturnCode; - pub fn inflateReset2(strm: z_streamp, windowBits: c_int) -> ReturnCode; - pub fn inflatePrime(strm: z_streamp, bits: c_int, value: c_int) -> ReturnCode; - pub fn inflateMark(strm: z_streamp) -> c_long; - pub fn inflateGetHeader(strm: z_streamp, head: gz_headerp) -> ReturnCode; - pub fn inflateBack( - strm: z_streamp, - in_: in_func, - in_desc: *mut c_void, - out: out_func, - out_desc: *mut c_void, - ) -> ReturnCode; - pub fn inflateBackEnd(strm: z_streamp) -> ReturnCode; - pub safe fn zlibCompileFlags() -> uLong; - pub fn compress( - dest: *mut Bytef, - destLen: *mut uLongf, - source: *const Bytef, - sourceLen: uLong, - ) -> ReturnCode; - pub fn compress2( - dest: *mut Bytef, - destLen: *mut uLongf, - source: *const Bytef, - sourceLen: uLong, - level: c_int, - ) -> ReturnCode; - pub safe fn compressBound(sourceLen: uLong) -> uLong; - pub fn uncompress( - dest: *mut Bytef, - destLen: *mut uLongf, - source: *const Bytef, - sourceLen: uLong, - ) -> ReturnCode; - pub fn uncompress2( - dest: *mut Bytef, - destLen: *mut uLongf, - source: *const Bytef, - sourceLen: *mut uLong, - ) -> ReturnCode; - pub fn gzdopen(fd: c_int, mode: *const u8) -> gzFile; - pub fn gzbuffer(file: gzFile, size: c_uint) -> ReturnCode; - pub fn gzsetparams(file: gzFile, level: c_int, strategy: c_int) -> ReturnCode; - pub fn gzread(file: gzFile, buf: voidp, len: c_uint) -> ReturnCode; - pub fn gzfread(buf: voidp, size: z_size_t, nitems: z_size_t, file: gzFile) -> z_size_t; - pub fn gzwrite(file: gzFile, buf: voidpc, len: c_uint) -> ReturnCode; - pub fn gzfwrite(buf: voidpc, size: z_size_t, nitems: z_size_t, file: gzFile) -> z_size_t; - pub fn gzprintf(file: gzFile, format: *const u8, ...) -> ReturnCode; - pub fn gzputs(file: gzFile, s: *const u8) -> ReturnCode; - pub fn gzgets(file: gzFile, buf: *mut u8, len: c_int) -> *mut u8; - pub fn gzputc(file: gzFile, c: c_int) -> ReturnCode; - pub fn gzgetc(file: gzFile) -> ReturnCode; - pub fn gzungetc(c: c_int, file: gzFile) -> ReturnCode; - pub fn gzflush(file: gzFile, flush: FlushValue) -> ReturnCode; - pub fn gzrewind(file: gzFile) -> ReturnCode; - pub fn gzeof(file: gzFile) -> ReturnCode; - pub fn gzdirect(file: gzFile) -> ReturnCode; - pub fn gzclose(file: gzFile) -> ReturnCode; - pub fn gzclose_r(file: gzFile) -> ReturnCode; - pub fn gzclose_w(file: gzFile) -> ReturnCode; - pub fn gzerror(file: gzFile, errnum: *mut c_int) -> *const u8; - pub fn gzclearerr(file: gzFile); - pub fn adler32(adler: uLong, buf: *const Bytef, len: uInt) -> uLong; - pub fn adler32_z(adler: uLong, buf: *const Bytef, len: z_size_t) -> uLong; - pub fn crc32(crc: uLong, buf: *const Bytef, len: uInt) -> uLong; - pub fn crc32_z(crc: uLong, buf: *const Bytef, len: z_size_t) -> uLong; - pub safe fn crc32_combine_op(crc1: uLong, crc2: uLong, op: uLong) -> uLong; - pub fn deflateInit_( - strm: z_streamp, - level: c_int, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn inflateInit_(strm: z_streamp, version: *const c_char, stream_size: c_int) -> ReturnCode; - pub fn deflateInit2_( - strm: z_streamp, - level: c_int, - method: c_int, - windowBits: c_int, - memLevel: c_int, - strategy: c_int, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn inflateInit2_( - strm: z_streamp, - windowBits: c_int, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn inflateBackInit_( - strm: z_streamp, - windowBits: c_int, - window: *mut u8, - version: *const c_char, - stream_size: c_int, - ) -> ReturnCode; - pub fn gzgetc_(file: gzFile) -> ReturnCode; - pub fn gzopen(path: *const u8, mode: *const u8) -> gzFile; - pub fn gzseek(file: gzFile, offset: c_long, whence: c_int) -> c_long; - pub fn gztell(file: gzFile) -> c_long; - pub fn gzoffset(file: gzFile) -> c_long; - pub safe fn adler32_combine(a: uLong, b: uLong, len: c_long) -> uLong; - pub safe fn crc32_combine(a: uLong, b: uLong, len: c_long) -> uLong; - pub safe fn crc32_combine_gen(len: c_long) -> uLong; - pub safe fn zError(err: c_int) -> *const u8; - pub fn inflateSyncPoint(strm: z_streamp) -> ReturnCode; - // pub fn get_crc_table() -> *const z_crc_t; - pub fn inflateUndermine(strm: z_streamp, subvert: c_int) -> ReturnCode; - pub fn inflateValidate(strm: z_streamp, check: c_int) -> ReturnCode; - pub fn inflateCodesUsed(strm: z_streamp) -> c_ulong; - pub fn inflateResetKeep(strm: z_streamp) -> ReturnCode; - pub fn deflateResetKeep(strm: z_streamp) -> ReturnCode; -} - -pub type z_off_t = c_long; -pub const Z_NO_FLUSH: c_int = 0; -pub const Z_PARTIAL_FLUSH: c_int = 1; -pub const Z_SYNC_FLUSH: c_int = 2; -pub const Z_FULL_FLUSH: c_int = 3; -pub const Z_FINISH: c_int = 4; -pub const Z_BLOCK: c_int = 5; -pub const Z_TREES: c_int = 6; -pub const Z_OK: c_int = 0; -pub const Z_STREAM_END: c_int = 1; -pub const Z_NEED_DICT: c_int = 2; -pub const Z_ERRNO: c_int = -1; -pub const Z_STREAM_ERROR: c_int = -2; -pub const Z_DATA_ERROR: c_int = -3; -pub const Z_MEM_ERROR: c_int = -4; -pub const Z_BUF_ERROR: c_int = -5; -pub const Z_VERSION_ERROR: c_int = -6; -pub const Z_NO_COMPRESSION: c_int = 0; -pub const Z_BEST_SPEED: c_int = 1; -pub const Z_BEST_COMPRESSION: c_int = 9; -pub const Z_DEFAULT_COMPRESSION: c_int = -1; -pub const Z_FILTERED: c_int = 1; -pub const Z_HUFFMAN_ONLY: c_int = 2; -pub const Z_RLE: c_int = 3; -pub const Z_FIXED: c_int = 4; -pub const Z_DEFAULT_STRATEGY: c_int = 0; -pub const Z_BINARY: c_int = 0; -pub const Z_TEXT: c_int = 1; -pub const Z_ASCII: c_int = Z_TEXT; -pub const Z_UNKNOWN: c_int = 2; -pub const Z_DEFLATED: c_int = 8; -pub const Z_NULL: c_int = 0; - -#[inline] -pub unsafe fn deflate_init(strm: z_streamp, level: c_int) -> ReturnCode { - // SAFETY: caller guarantees `strm` is a valid z_stream pointer; zlib reads version/stream_size for ABI check. - unsafe { - deflateInit_( - strm, - level, - zlibVersion(), - c_int::try_from(core::mem::size_of::()).expect("int cast"), - ) - } -} -#[inline] -pub unsafe fn inflate_init(strm: z_streamp) -> ReturnCode { - // SAFETY: caller guarantees `strm` is a valid z_stream pointer. - unsafe { - inflateInit_( - strm, - zlibVersion(), - c_int::try_from(core::mem::size_of::()).expect("int cast"), - ) - } -} -#[inline] -pub unsafe fn deflate_init2( - strm: z_streamp, - level: c_int, - method: c_int, - window_bits: c_int, - mem_level: c_int, - strategy: c_int, -) -> ReturnCode { - // SAFETY: caller guarantees `strm` is a valid z_stream pointer. - unsafe { - deflateInit2_( - strm, - level, - method, - window_bits, - mem_level, - strategy, - zlibVersion(), - c_int::try_from(core::mem::size_of::()).expect("int cast"), - ) - } -} -#[inline] -pub unsafe fn inflate_init2(strm: z_streamp, window_bits: c_int) -> ReturnCode { - // SAFETY: caller guarantees `strm` is a valid z_stream pointer. - unsafe { - inflateInit2_( - strm, - window_bits, - zlibVersion(), - c_int::try_from(core::mem::size_of::()).expect("int cast"), - ) - } -} -#[inline] -pub unsafe fn inflate_back_init( - strm: z_streamp, - window_bits: c_int, - window: *mut u8, -) -> ReturnCode { - // SAFETY: caller guarantees `strm` and `window` are valid. - unsafe { - inflateBackInit_( - strm, - window_bits, - window, - zlibVersion(), - c_int::try_from(core::mem::size_of::()).expect("int cast"), - ) - } -} - -pub type gz_header_s = struct_gz_header_s;