From 6da7ffb0a8430fec7bae08809e6af22eaf0131aa Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 17 Aug 2026 16:14:19 +0000 Subject: [PATCH 1/6] Remove the legacy inbound frame path from h2_frame_parser.rs and the HPACK common-strings cache it fed --- src/jsc/bindings/BunHttp2CommonStrings.cpp | 37 - src/jsc/bindings/BunHttp2CommonStrings.h | 105 - src/jsc/bindings/ZigGlobalObject.cpp | 9 - src/jsc/bindings/ZigGlobalObject.h | 3 - src/runtime/api/bun/h2_frame_parser.rs | 2264 +---------------- ...ad-symbols-ffi-sys-test-runner-cpp.test.ts | 1 - 6 files changed, 58 insertions(+), 2361 deletions(-) delete mode 100644 src/jsc/bindings/BunHttp2CommonStrings.cpp delete mode 100644 src/jsc/bindings/BunHttp2CommonStrings.h diff --git a/src/jsc/bindings/BunHttp2CommonStrings.cpp b/src/jsc/bindings/BunHttp2CommonStrings.cpp deleted file mode 100644 index e1eba23d6aa1..000000000000 --- a/src/jsc/bindings/BunHttp2CommonStrings.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "root.h" -#include "BunHttp2CommonStrings.h" -#include -#include -#include -#include -#include "ZigGlobalObject.h" -#include -#include - -namespace Bun { -using namespace JSC; - -#define HTTP2_COMMON_STRINGS_LAZY_PROPERTY_DEFINITION(jsName, key, value, idx) \ - this->m_names[idx].initLater( \ - [](const JSC::LazyProperty::Initializer& init) { \ - init.set(jsOwnedString(init.vm, key)); \ - }); - -#define HTTP2_COMMON_STRINGS_LAZY_PROPERTY_VISITOR(name, key, value, idx) \ - this->m_names[idx].visit(visitor); - -void Http2CommonStrings::initialize() -{ - HTTP2_COMMON_STRINGS_EACH_NAME(HTTP2_COMMON_STRINGS_LAZY_PROPERTY_DEFINITION) -} - -template -void Http2CommonStrings::visit(Visitor& visitor) -{ - HTTP2_COMMON_STRINGS_EACH_NAME(HTTP2_COMMON_STRINGS_LAZY_PROPERTY_VISITOR) -} - -template void Http2CommonStrings::visit(JSC::AbstractSlotVisitor&); -template void Http2CommonStrings::visit(JSC::SlotVisitor&); - -} // namespace Bun diff --git a/src/jsc/bindings/BunHttp2CommonStrings.h b/src/jsc/bindings/BunHttp2CommonStrings.h deleted file mode 100644 index 169a4b6efbde..000000000000 --- a/src/jsc/bindings/BunHttp2CommonStrings.h +++ /dev/null @@ -1,105 +0,0 @@ -#pragma once - -// clang-format off - -#define HTTP2_COMMON_STRINGS_EACH_NAME(macro) \ - macro(authority, ":authority"_s, ""_s, 0) \ -macro(methodGet, ":method"_s, "GET"_s, 1) \ -macro(methodPost, ":method"_s, "POST"_s, 2) \ -macro(pathRoot, ":path"_s, "/"_s, 3) \ -macro(pathIndex, ":path"_s, "/index.html"_s, 4) \ -macro(schemeHttp, ":scheme"_s, "http"_s, 5) \ -macro(schemeHttps, ":scheme"_s, "https"_s, 6) \ -macro(status200, ":status"_s, "200"_s, 7) \ -macro(status204, ":status"_s, "204"_s, 8) \ -macro(status206, ":status"_s, "206"_s, 9) \ -macro(status304, ":status"_s, "304"_s, 10) \ -macro(status400, ":status"_s, "400"_s, 11) \ -macro(status404, ":status"_s, "404"_s, 12) \ -macro(status500, ":status"_s, "500"_s, 13) \ -macro(acceptCharset, "accept-charset"_s, ""_s, 14) \ -macro(acceptEncoding, "accept-encoding"_s, "gzip, deflate"_s, 15) \ -macro(acceptLanguage, "accept-language"_s, ""_s, 16) \ -macro(acceptRanges, "accept-ranges"_s, ""_s, 17) \ -macro(accept, "accept"_s, ""_s, 18) \ -macro(accessControlAllowOrigin, "access-control-allow-origin"_s, ""_s, 19) \ -macro(age, "age"_s, ""_s, 20) \ -macro(allow, "allow"_s, ""_s, 21) \ -macro(authorization, "authorization"_s, ""_s, 22) \ -macro(cacheControl, "cache-control"_s, ""_s, 23) \ -macro(contentDisposition, "content-disposition"_s, ""_s, 24) \ -macro(contentEncoding, "content-encoding"_s, ""_s, 25) \ -macro(contentLanguage, "content-language"_s, ""_s, 26) \ -macro(contentLength, "content-length"_s, ""_s, 27) \ -macro(contentLocation, "content-location"_s, ""_s, 28) \ -macro(contentRange, "content-range"_s, ""_s, 29) \ -macro(contentType, "content-type"_s, ""_s, 30) \ -macro(cookie, "cookie"_s, ""_s, 31) \ -macro(date, "date"_s, ""_s, 32) \ -macro(etag, "etag"_s, ""_s, 33) \ -macro(expect, "expect"_s, ""_s, 34) \ -macro(expires, "expires"_s, ""_s, 35) \ -macro(from, "from"_s, ""_s, 36) \ -macro(host, "host"_s, ""_s, 37) \ -macro(ifMatch, "if-match"_s, ""_s, 38) \ -macro(ifModifiedSince, "if-modified-since"_s, ""_s, 39) \ -macro(ifNoneMatch, "if-none-match"_s, ""_s, 40) \ -macro(ifRange, "if-range"_s, ""_s, 41) \ -macro(ifUnmodifiedSince, "if-unmodified-since"_s, ""_s, 42) \ -macro(lastModified, "last-modified"_s, ""_s, 43) \ -macro(link, "link"_s, ""_s, 44) \ -macro(location, "location"_s, ""_s, 45) \ -macro(maxForwards, "max-forwards"_s, ""_s, 46) \ -macro(proxyAuthenticate, "proxy-authenticate"_s, ""_s, 47) \ -macro(proxyAuthorization, "proxy-authorization"_s, ""_s, 48) \ -macro(range, "range"_s, ""_s, 49) \ -macro(referer, "referer"_s, ""_s, 50) \ -macro(refresh, "refresh"_s, ""_s, 51) \ -macro(retryAfter, "retry-after"_s, ""_s, 52) \ -macro(server, "server"_s, ""_s, 53) \ -macro(setCookie, "set-cookie"_s, ""_s, 54) \ -macro(strictTransportSecurity, "strict-transport-security"_s, ""_s, 55) \ -macro(transferEncoding, "transfer-encoding"_s, ""_s, 56) \ -macro(userAgent, "user-agent"_s, ""_s, 57) \ -macro(vary, "vary"_s, ""_s, 58) \ -macro(via, "via"_s, ""_s, 59) \ -macro(wwwAuthenticate, "www-authenticate"_s, ""_s, 60) - -// clang-format on - -#define HTTP2_COMMON_STRINGS_ACCESSOR_DEFINITION(name, key, value, idx) \ - JSC::JSString* name##String(JSC::JSGlobalObject* globalObject) \ - { \ - return m_names[idx].getInitializedOnMainThread(globalObject); \ - } - -namespace Bun { - -using namespace JSC; - -class Http2CommonStrings { - -public: - HTTP2_COMMON_STRINGS_EACH_NAME(HTTP2_COMMON_STRINGS_ACCESSOR_DEFINITION) - - void initialize(); - - template - void visit(Visitor& visitor); - - JSC::JSString* getStringFromHPackIndex(uint16_t index, JSC::JSGlobalObject* globalObject) - { - if (index > 60) { - return nullptr; - } - return m_names[index].getInitializedOnMainThread(globalObject); - } - -private: - JSC::LazyProperty m_names[61]; -}; - -} // namespace Bun - -#undef BUN_COMMON_STRINGS_ACCESSOR_DEFINITION -#undef BUN_COMMON_STRINGS_LAZY_PROPERTY_DECLARATION diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index b5252a9fbe43..7397b485d319 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -2018,7 +2018,6 @@ void GlobalObject::finishCreation(VM& vm) ASSERT(inherits(info())); m_commonStrings.initialize(); - m_http2CommonStrings.initialize(); m_bakeAdditions.initialize(); m_markdownTagStrings.initialize(); @@ -3342,14 +3341,6 @@ extern "C" uint8_t JSC__JSGlobalObject__drainMicrotasks(Zig::GlobalObject* globa return 2; return globalObject->drainMicrotasks(); } -extern "C" EncodedJSValue JSC__JSGlobalObject__getHTTP2CommonString(Zig::GlobalObject* globalObject, uint32_t hpack_index) -{ - auto value = globalObject->http2CommonStrings().getStringFromHPackIndex(hpack_index, globalObject); - if (value != nullptr) { - return JSValue::encode(value); - } - return JSValue::encode(JSValue::JSUndefined); -} template static void visitGlobalObjectMember(Visitor& visitor, T& anything) { diff --git a/src/jsc/bindings/ZigGlobalObject.h b/src/jsc/bindings/ZigGlobalObject.h index 09559a1e4b5a..9c5b615cab57 100644 --- a/src/jsc/bindings/ZigGlobalObject.h +++ b/src/jsc/bindings/ZigGlobalObject.h @@ -64,7 +64,6 @@ struct node_module; #include "InternalModuleRegistry.h" #include "headers-handwritten.h" #include "BunCommonStrings.h" -#include "BunHttp2CommonStrings.h" #include "BunMarkdownTagStrings.h" #include "BunGlobalScope.h" #include @@ -576,7 +575,6 @@ class GlobalObject : public Bun::GlobalScope { V(private, std::unique_ptr, m_builtinInternalFunctions) \ V(private, std::unique_ptr, m_constructors) \ V(private, Bun::CommonStrings, m_commonStrings) \ - V(private, Bun::Http2CommonStrings, m_http2CommonStrings) \ V(private, Bun::MarkdownTagStrings, m_markdownTagStrings) \ \ /* JSC's hashtable code-generator tries to access these properties, so we make them public. */ \ @@ -798,7 +796,6 @@ class GlobalObject : public Bun::GlobalScope { void setNodeWorkerEntryEvaluatedHook(JSObject* hook); Bun::CommonStrings& commonStrings() { return m_commonStrings; } - Bun::Http2CommonStrings& http2CommonStrings() { return m_http2CommonStrings; } Bun::MarkdownTagStrings& markdownTagStrings() { return m_markdownTagStrings; } #include "ZigGeneratedClasses+lazyStructureHeader.h" diff --git a/src/runtime/api/bun/h2_frame_parser.rs b/src/runtime/api/bun/h2_frame_parser.rs index ec5cb1a3fd94..efe73a36f93c 100644 --- a/src/runtime/api/bun/h2_frame_parser.rs +++ b/src/runtime/api/bun/h2_frame_parser.rs @@ -1,9 +1,5 @@ //! HTTP/2 frame parser. #![allow( - // Transitional: the legacy inbound half of this file is dead now that the rewrite engine - // (src/runtime/api/bun/h2) serves all inbound traffic; it is removed together with the - // outbound migration. Until then, suppress dead-code for the file. - dead_code, non_camel_case_types, non_upper_case_globals, clippy::too_many_arguments @@ -21,7 +17,6 @@ use crate::socket::NativeCallbacks; use crate::webcore::AutoFlusher; use bstr::BStr; use bun_collections::{ByteVecExt, HashMap as BunHashMap, HiveArrayFallback, VecExt}; -use bun_core::MutableString; use bun_core::String as BunString; use bun_core::strings; use bun_http::lshpack; @@ -118,10 +113,6 @@ enum BunSocket { } unsafe extern "C" { - safe fn JSC__JSGlobalObject__getHTTP2CommonString( - global_object: &JSGlobalObject, - hpack_index: u32, - ) -> JSValue; safe fn Bun__wrapAbortError(global_object: &JSGlobalObject, cause: JSValue) -> JSValue; /// One-call materialization of a decoded header block: returns the /// [rawHeadersArray, headersObject, sensitiveArray|undefined] tuple, or a @@ -181,20 +172,6 @@ impl H2GlobalErrExt for JSGlobalObject { } } -pub(crate) fn get_http2_common_string( - global_object: &JSGlobalObject, - hpack_index: u32, -) -> Option { - if hpack_index == 255 { - return None; - } - let value = JSC__JSGlobalObject__getHTTP2CommonString(global_object, hpack_index); - if value.is_empty_or_undefined_or_null() { - return None; - } - Some(value) -} - const MAX_WINDOW_SIZE: u32 = i32::MAX as u32; const MAX_HEADER_TABLE_SIZE: u32 = u32::MAX; const MAX_STREAM_ID: u32 = i32::MAX as u32; @@ -272,11 +249,6 @@ enum HeadersFrameFlags { PRIORITY = 0x20, } -#[repr(u8)] -enum SettingsFlags { - ACK = 0x1, -} - // Open set of wire values → newtype over u32 #[repr(transparent)] #[derive(Clone, Copy, PartialEq, Eq)] @@ -285,7 +257,6 @@ impl ErrorCode { const NO_ERROR: Self = Self(0x0); const PROTOCOL_ERROR: Self = Self(0x1); const INTERNAL_ERROR: Self = Self(0x2); - const FLOW_CONTROL_ERROR: Self = Self(0x3); const FRAME_SIZE_ERROR: Self = Self(0x6); const REFUSED_STREAM: Self = Self(0x7); const CANCEL: Self = Self(0x8); @@ -294,27 +265,6 @@ impl ErrorCode { const MAX_PENDING_SETTINGS_ACK: Self = Self(0xe); } -// Open set of wire values → newtype over u16 -#[repr(transparent)] -#[derive(Clone, Copy, PartialEq, Eq)] -struct SettingsType(u16); -impl SettingsType { - const SETTINGS_HEADER_TABLE_SIZE: Self = Self(0x1); - const SETTINGS_ENABLE_PUSH: Self = Self(0x2); - const SETTINGS_MAX_CONCURRENT_STREAMS: Self = Self(0x3); - const SETTINGS_INITIAL_WINDOW_SIZE: Self = Self(0x4); - const SETTINGS_MAX_FRAME_SIZE: Self = Self(0x5); - const SETTINGS_MAX_HEADER_LIST_SIZE: Self = Self(0x6); - // non standard extension settings here (we still dont support this ones) - const SETTINGS_ENABLE_CONNECT_PROTOCOL: Self = Self(0x8); -} - -#[inline] -fn u32_from_bytes(src: &[u8]) -> u32 { - debug_assert!(src.len() == 4); - u32::from_be_bytes(src[0..4].try_into().expect("infallible: size matches")) -} - // ────────────────────────────────────────────────────────────────────────── // Packed wire structs // ────────────────────────────────────────────────────────────────────────── @@ -333,25 +283,17 @@ impl UInt31WithReserved { self.0 & 0x7fff_ffff } #[inline] - fn from(value: u32) -> Self { - Self(value) - } - #[inline] fn init(value: u32, reserved: bool) -> Self { Self((value & 0x7fff_ffff) | if reserved { 0x8000_0000 } else { 0 }) } /// Note: the wire format (RFC 7540 §6.3) wants the reserved/E bit at bit /// 31, so the layout is `(reserved << 31) | uint31`, which matches - /// `from_bytes`/`write` and the on-wire `StreamPriority.stream_identifier`. + /// `write` and the on-wire `StreamPriority.stream_identifier`. #[inline] fn to_uint32(self) -> u32 { self.0 } #[inline] - fn from_bytes(src: &[u8]) -> Self { - Self(u32_from_bytes(src)) - } - #[inline] fn write(self, writer: &mut impl WireWriter) -> bool { let mut value: u32 = self.uint31(); if self.reserved() { @@ -383,18 +325,6 @@ impl StreamPriority { swap.stream_identifier = swap.stream_identifier.swap_bytes(); writer.write_all(bytemuck::bytes_of(&swap)).is_ok() } - #[inline] - fn from(dst: &mut StreamPriority, src: &[u8]) { - // SAFETY: src.len() == BYTE_SIZE asserted by caller - unsafe { - core::ptr::copy_nonoverlapping( - src.as_ptr(), - std::ptr::from_mut(dst).cast::(), - Self::BYTE_SIZE, - ); - } - dst.stream_identifier = dst.stream_identifier.swap_bytes(); - } } // packed struct(u72): length: u24, type: u8, flags: u8, streamIdentifier: u32 @@ -447,84 +377,32 @@ impl FrameHeader { } } -// packed struct(u48): type: u16, value: u32 -#[repr(C, packed)] -#[derive(Clone, Copy, Default)] -pub struct SettingsPayloadUnit { - type_: u16, - value: u32, -} -impl SettingsPayloadUnit { - pub const BYTE_SIZE: usize = 6; - #[inline] - fn from(dst: &mut SettingsPayloadUnit, src: &[u8], offset: usize) { - // SAFETY: caller guarantees src.len() + offset <= BYTE_SIZE - unsafe { - core::ptr::copy_nonoverlapping( - src.as_ptr(), - std::ptr::from_mut(dst).cast::().add(offset), - src.len(), - ); - } - if END { - dst.type_ = u16::swap_bytes(dst.type_); - dst.value = u32::swap_bytes(dst.value); - } - } -} - -// packed struct(u336) — 7 × (u16 type + u32 value) = 42 bytes -// Wire layout via #[repr(C, packed)]: all fields are byte-aligned u16/u32, and -// the per-field swap_bytes() in write() swaps each field individually, not the -// whole backing int. -#[repr(C, packed)] +/// The seven standard SETTINGS parameters (RFC 9113 §6.5.2). The wire form is produced by +/// `write_settings_payload`, which only emits the entries JS set explicitly. #[derive(Clone, Copy)] pub(crate) struct FullSettingsPayload { - _header_table_size_type: u16, header_table_size: u32, - _enable_push_type: u16, enable_push: u32, - _max_concurrent_streams_type: u16, max_concurrent_streams: u32, - _initial_window_size_type: u16, initial_window_size: u32, - _max_frame_size_type: u16, max_frame_size: u32, - _max_header_list_size_type: u16, max_header_list_size: u32, - _enable_connect_protocol_type: u16, enable_connect_protocol: u32, } -// SAFETY: `#[repr(C, packed)]` with only `u16`/`u32` fields — no padding, no -// niches, every 42-byte pattern is a valid value. -unsafe impl bytemuck::Zeroable for FullSettingsPayload {} -// SAFETY: see `Zeroable` impl above; additionally `Copy + 'static`. -unsafe impl bytemuck::Pod for FullSettingsPayload {} -const _: () = - assert!(core::mem::size_of::() == FullSettingsPayload::BYTE_SIZE); impl Default for FullSettingsPayload { fn default() -> Self { Self { - _header_table_size_type: SettingsType::SETTINGS_HEADER_TABLE_SIZE.0, header_table_size: 4096, - _enable_push_type: SettingsType::SETTINGS_ENABLE_PUSH.0, enable_push: 1, - _max_concurrent_streams_type: SettingsType::SETTINGS_MAX_CONCURRENT_STREAMS.0, max_concurrent_streams: 4294967295, - _initial_window_size_type: SettingsType::SETTINGS_INITIAL_WINDOW_SIZE.0, initial_window_size: 65535, - _max_frame_size_type: SettingsType::SETTINGS_MAX_FRAME_SIZE.0, max_frame_size: 16384, - _max_header_list_size_type: SettingsType::SETTINGS_MAX_HEADER_LIST_SIZE.0, max_header_list_size: 65535, - _enable_connect_protocol_type: SettingsType::SETTINGS_ENABLE_CONNECT_PROTOCOL.0, enable_connect_protocol: 0, } } } impl FullSettingsPayload { - pub(crate) const BYTE_SIZE: usize = 42; - pub(crate) fn to_engine_settings(&self) -> crate::api::h2::settings::Settings { crate::api::h2::settings::Settings { header_table_size: self.header_table_size, @@ -539,7 +417,6 @@ impl FullSettingsPayload { pub(crate) fn to_js(&self, global_object: &JSGlobalObject) -> JSValue { let result = JSValue::create_empty_object(global_object, 8); - // Packed-field reads are by-value (Copy) → no unaligned-ref hazard. let header_table_size = self.header_table_size; let enable_push = self.enable_push; let max_concurrent_streams = self.max_concurrent_streams; @@ -585,42 +462,6 @@ impl FullSettingsPayload { ); result } - - pub(crate) fn update_with(&mut self, option: SettingsPayloadUnit) { - match SettingsType(option.type_) { - SettingsType::SETTINGS_HEADER_TABLE_SIZE => self.header_table_size = option.value, - SettingsType::SETTINGS_ENABLE_PUSH => self.enable_push = option.value, - SettingsType::SETTINGS_MAX_CONCURRENT_STREAMS => { - self.max_concurrent_streams = option.value - } - SettingsType::SETTINGS_INITIAL_WINDOW_SIZE => self.initial_window_size = option.value, - SettingsType::SETTINGS_MAX_FRAME_SIZE => self.max_frame_size = option.value, - SettingsType::SETTINGS_MAX_HEADER_LIST_SIZE => self.max_header_list_size = option.value, - SettingsType::SETTINGS_ENABLE_CONNECT_PROTOCOL => { - self.enable_connect_protocol = option.value - } - _ => {} - } - } - - pub(crate) fn write(&self, writer: &mut impl WireWriter) -> bool { - let mut swap = *self; - swap._header_table_size_type = swap._header_table_size_type.swap_bytes(); - swap.header_table_size = swap.header_table_size.swap_bytes(); - swap._enable_push_type = swap._enable_push_type.swap_bytes(); - swap.enable_push = swap.enable_push.swap_bytes(); - swap._max_concurrent_streams_type = swap._max_concurrent_streams_type.swap_bytes(); - swap.max_concurrent_streams = swap.max_concurrent_streams.swap_bytes(); - swap._initial_window_size_type = swap._initial_window_size_type.swap_bytes(); - swap.initial_window_size = swap.initial_window_size.swap_bytes(); - swap._max_frame_size_type = swap._max_frame_size_type.swap_bytes(); - swap.max_frame_size = swap.max_frame_size.swap_bytes(); - swap._max_header_list_size_type = swap._max_header_list_size_type.swap_bytes(); - swap.max_header_list_size = swap.max_header_list_size.swap_bytes(); - swap._enable_connect_protocol_type = swap._enable_connect_protocol_type.swap_bytes(); - swap.enable_connect_protocol = swap.enable_connect_protocol.swap_bytes(); - writer.write_all(bytemuck::bytes_of(&swap)).is_ok() - } } /// Writer trait used for generic wire-serialization writer params. @@ -1294,12 +1135,6 @@ pub struct H2FrameParser { enforced_max_header_list_size: Cell, // only available after receiving settings or ACK remote_settings: Cell>, - // current frame being read - current_frame: Cell>, - // remaining bytes to read for the current frame - remaining_length: Cell, - // buffer if more data is needed for the current frame - read_buffer: JsCell, // local Window limits the download of data // current window size for the connection @@ -1357,13 +1192,9 @@ pub struct H2FrameParser { /// nghttp2 servers reject a GOAWAY naming a client-initiated id with a connection /// PROTOCOL_ERROR (node's last_proc_stream_id semantics). last_peer_stream_id: Cell, - // Stream id whose header block is awaiting CONTINUATION frames - // (RFC 9113 §4.3); 0 when none. - expecting_continuation: Cell, is_server: Cell, /// A frame callback left an exception pending in this batch (`Sink::should_stop`). left_exception: Cell, - preface_received_len: Cell, // we buffer requests until we get the first settings ACK write_buffer: JsCell>, write_buffer_offset: Cell, @@ -1538,16 +1369,6 @@ pub struct Stream { js_context: StrongOptional, // jsc.Strong.Optional wait_for_trailers: bool, end_after_headers: bool, - is_waiting_more_headers: bool, - header_block_size: usize, - header_block_count: usize, - // Header block fragments buffered across HEADERS + CONTINUATION until - // END_HEADERS arrives (RFC 9113 §4.3); capped at `max_header_list_size`. - pending_header_block: Vec, - // Flags from the HEADERS frame that started `pending_header_block`; - // CONTINUATION frames only carry END_HEADERS. - pending_header_flags: u8, - padding: Option, padding_strategy: PaddingStrategy, rst_code: u32, stream_dependency: u32, @@ -2086,12 +1907,6 @@ impl Stream { js_context: StrongOptional::empty(), wait_for_trailers: false, end_after_headers: false, - is_waiting_more_headers: false, - header_block_size: 0, - header_block_count: 0, - pending_header_block: Vec::new(), - pending_header_flags: 0, - padding: None, padding_strategy, rst_code: 0, stream_dependency: 0, @@ -2235,8 +2050,6 @@ impl AbortListener for SignalRef { } } -type HeaderValue = lshpack::DecodeResult; - // ────────────────────────────────────────────────────────────────────────── // H2FrameParser impl — core methods // ────────────────────────────────────────────────────────────────────────── @@ -2278,15 +2091,6 @@ impl H2FrameParser { } } - pub(crate) fn decode(&self, src_buffer: &[u8]) -> crate::Result { - self.hpack.with_mut(|hpack| { - if let Some(hpack) = hpack.as_mut() { - return hpack.decode(src_buffer).map_err(crate::Error::from); - } - Err(crate::Error::UnableToDecode) - }) - } - pub(crate) fn encode( &self, dst_buffer: &mut [u8], @@ -2306,97 +2110,6 @@ impl H2FrameParser { }) } - /// Calculate the new window size for the connection and the stream - /// https://datatracker.ietf.org/doc/html/rfc7540#section-6.9.1 - fn adjust_window_size(&self, stream: Option<&mut Stream>, payload_size: u32) { - self.used_window_size.set( - self.used_window_size - .get() - .saturating_add(payload_size as u64), - ); - bun_output::scoped_log!( - H2FrameParser, - "adjustWindowSize {} {} {} {}", - self.used_window_size.get(), - self.window_size.get(), - self.is_server.get(), - payload_size - ); - if self.used_window_size.get() > self.window_size.get() { - // we are receiving more data than we are allowed to - self.send_go_away( - 0, - ErrorCode::FLOW_CONTROL_ERROR, - b"Window size overflow", - self.last_stream_id.get(), - true, - ); - self.used_window_size - .set(self.used_window_size.get() - payload_size as u64); - } - - if let Some(s) = stream { - s.used_window_size += payload_size as u64; - if s.used_window_size > s.window_size { - // we are receiving more data than we are allowed to - self.send_go_away( - s.id, - ErrorCode::FLOW_CONTROL_ERROR, - b"Window size overflow", - self.last_stream_id.get(), - true, - ); - s.used_window_size -= payload_size as u64; - } - } - } - - fn increment_window_size_if_needed(&self) { - // Note: reshaped for borrowck — collect actions then apply - let mut updates: Vec<(u32, u64)> = Vec::new(); - for (_, item) in self.streams.get().iter() { - // SAFETY: item is &*mut Stream from streams.iter(); the boxed Stream outlives the iteration - let stream = unsafe { &mut **item }; - bun_output::scoped_log!( - H2FrameParser, - "incrementWindowSizeIfNeeded stream {} {} {} {}", - stream.id, - stream.used_window_size, - stream.window_size, - self.is_server.get() - ); - if stream.used_window_size >= stream.window_size / 2 && stream.used_window_size > 0 { - let consumed = stream.used_window_size; - stream.used_window_size = 0; - bun_output::scoped_log!( - H2FrameParser, - "incrementWindowSizeIfNeeded stream {} {} {}", - stream.id, - stream.window_size, - self.is_server.get() - ); - updates.push((stream.id, consumed)); - } - } - for (id, consumed) in updates { - self.send_window_update(id, UInt31WithReserved::init(consumed as u32, false)); - } - bun_output::scoped_log!( - H2FrameParser, - "incrementWindowSizeIfNeeded connection {} {} {}", - self.used_window_size.get(), - self.window_size.get(), - self.is_server.get() - ); - if self.used_window_size.get() >= self.window_size.get() / 2 - && self.used_window_size.get() > 0 - { - let consumed = self.used_window_size.get(); - self.used_window_size.set(0); - self.send_window_update(0, UInt31WithReserved::init(consumed as u32, false)); - } - } - /// Serialize the SETTINGS entries that go on the wire: only the standard parameters JS set /// explicitly plus any custom settings (node never serializes defaults — a session created /// with default options sends an empty SETTINGS frame). Returns the payload length. @@ -2715,20 +2428,6 @@ impl H2FrameParser { let _ = self.write(&preface_buffer[..24 + FrameHeader::BYTE_SIZE + payload_len]); } - pub(crate) fn send_settings_ack(&self) { - bun_output::scoped_log!(H2FrameParser, "send HTTP_FRAME_SETTINGS ack true"); - let mut buffer = [0u8; FrameHeader::BYTE_SIZE]; - let mut stream = FixedBufferStream::new(&mut buffer); - let settings_header = FrameHeader { - type_: FrameType::HTTP_FRAME_SETTINGS as u8, - flags: SettingsFlags::ACK as u8, - stream_identifier: 0, - length: 0, - }; - let _ = settings_header.write(&mut stream, &self.frames_sent_legacy); - let _ = self.write(&buffer); - } - pub(crate) fn send_window_update( &self, stream_identifier: u32, @@ -2859,33 +2558,6 @@ impl H2FrameParser { ); } - pub(crate) fn dispatch_with_3_extra( - &self, - event: JSH2FrameParser::Gc, - value: JSValue, - extra: JSValue, - extra2: JSValue, - extra3: JSValue, - ) { - let Some(this_value) = self.strong_this.get().try_get() else { - return; - }; - let Some(ctx_value) = JSH2FrameParser::Gc::context.get(this_value) else { - return; - }; - value.ensure_still_alive(); - extra.ensure_still_alive(); - extra2.ensure_still_alive(); - extra3.ensure_still_alive(); - let _dispatch = self.enter_dispatch(); - let _ = self.handlers.get().call_event_handler( - event, - this_value, - ctx_value, - &[ctx_value, value, extra, extra2, extra3], - ); - } - /// A header block the HPACK encoder cannot emit fails the whole session in nghttp2, so node /// reports ERR_HTTP2_SESSION_ERROR (COMPRESSION_ERROR) rather than resetting the stream. /// The stream is left open for the session teardown to error, matching node's request error. @@ -3694,42 +3366,6 @@ impl H2FrameParser { } } -// Note: raw-ptr slice — the payload may alias `this.readBuffer` across -// `readBuffer.reset()` (e.g. handleHeadersFrame resets then calls decodeHeaderBlock(payload)). -// A borrowed `&'a [u8]` tied to `&'a mut self` forces every caller into an aliasing -// `unsafe { &mut *self_ptr }` reborrow, which under Stacked Borrows invalidates the slice the -// moment the caller touches `self` again. Carrying a raw pointer keeps the aliasing workable -// without materialising overlapping `&mut` borrows. -pub(crate) struct Payload { - data_ptr: *const u8, - data_len: usize, - end: usize, -} - -impl Payload { - /// Re-borrow the payload bytes as `&[u8]`, tied to `&self`. - /// - /// # Safety (encapsulated) - /// `data_ptr`/`data_len` describe a slice into either the caller-supplied `data` (alive for - /// the handler body) or `H2FrameParser.read_buffer.list`'s backing allocation. Both outlive - /// the local `Payload` returned by `handle_incomming_payload`: the caller's `data` lives for - /// the entire handler body, and `read_buffer` is never grown/freed between obtaining the - /// `Payload` and the last use of the returned slice. `read_buffer.reset()` is permitted: - /// `data_ptr` is derived via `Vec::as_mut_ptr()` (raw-ptr method, no intermediate `&[u8]` - /// borrow), which is documented to remain valid across non-reallocating mutation, so under - /// Stacked Borrows the `Vec::clear()` inside `reset()` does not invalidate it and the bytes - /// remain readable (several handlers reset before consuming - /// `payload`). The returned borrow is tied to the local `Payload` (not `self: H2FrameParser`), - /// so `&mut self` operations on the parser do not conflict with it under borrowck. - #[inline] - fn data(&self) -> &[u8] { - // SAFETY: see doc comment above — `data_ptr` is valid for `data_len` bytes for the - // full lifetime of this `Payload` local. `ffi::slice` tolerates the (null, 0) shape - // used for empty payloads. - unsafe { bun_core::ffi::slice(self.data_ptr, self.data_len) } - } -} - /// Trait to abstract over TLSSocket / TCPSocket for `generic_flush`/`generic_write`. pub(crate) trait NativeSocketWrite { fn write_maybe_corked(&mut self, buf: &[u8]) -> i32; @@ -3767,1596 +3403,72 @@ extern "C" fn on_auto_flush_trampoline(ctx: *mut c_void) -> bool { // are inherent methods now.) // ────────────────────────────────────────────────────────────────────────── -// H2FrameParser impl — frame handlers +// H2FrameParser impl — stream bookkeeping and engine Sink helpers // ────────────────────────────────────────────────────────────────────────── impl H2FrameParser { - // Default handling for payload is buffering it - // for data frames we use another strategy - pub(crate) fn handle_incomming_payload( - &self, - data: &[u8], - stream_identifier: u32, - ) -> Option { - let end: usize = (self.remaining_length.get() as usize).min(data.len()); - let payload = &data[0..end]; - self.remaining_length - .set(self.remaining_length.get() - i32::try_from(end).expect("int cast")); - if self.remaining_length.get() > 0 { - // buffer more data - let _ = self.read_buffer.with_mut(|rb| rb.append_slice(payload)); - self.global() - .vm() - .deprecated_report_extra_memory(payload.len()); - return None; - } else if self.remaining_length.get() < 0 { - self.send_go_away( - stream_identifier, - ErrorCode::FRAME_SIZE_ERROR, - b"Invalid frame size", - self.last_stream_id.get(), - true, - ); - return None; - } - - self.current_frame.set(None); - - if !self.read_buffer.get().list.is_empty() { - // return buffered data - let _ = self.read_buffer.with_mut(|rb| rb.append_slice(payload)); - self.global() - .vm() - .deprecated_report_extra_memory(payload.len()); - - // SAFETY contract for Payload::data: derive via Vec::as_mut_ptr() (raw-ptr method, - // no intermediate &[u8]) so the provenance survives `read_buffer.reset()` — - // Vec::clear() forms `&mut [u8]` internally, which under Stacked Borrows would pop a - // SharedReadOnly tag obtained from `as_slice().as_ptr()`. Several handlers - // (origin/altsvc/continuation/headers) read `payload` AFTER reset(), - // so the pointer must outlive that mutation. R-2: `JsCell` is - // `UnsafeCell`-backed; deriving the pointer via `with_mut` keeps SharedReadWrite - // provenance through later `read_buffer` accesses. - let (data_ptr, data_len) = self.read_buffer.with_mut(|rb| { - let list = &mut rb.list; - (list.as_mut_ptr().cast_const(), list.len()) - }); - return Some(Payload { - data_ptr, - data_len, - end, - }); - } - - Some(Payload { - data_ptr: payload.as_ptr(), - data_len: payload.len(), - end, - }) - } - - pub(crate) fn handle_window_update_frame( - &self, - frame: FrameHeader, - data: &[u8], - stream: Option<*mut Stream>, - ) -> usize { - bun_output::scoped_log!( - H2FrameParser, - "handleWindowUpdateFrame {}", - frame.stream_identifier - ); - // must be always 4 bytes (https://datatracker.ietf.org/doc/html/rfc7540#section-6.9) - if frame.length != 4 { - self.send_go_away( - frame.stream_identifier, - ErrorCode::FRAME_SIZE_ERROR, - b"Invalid dataframe frame size", - self.last_stream_id.get(), - true, - ); - return data.len(); - } - - if let Some(content) = self.handle_incomming_payload(data, frame.stream_identifier) { - let payload = content.data(); - let window_size_increment = UInt31WithReserved::from_bytes(payload); - let end = content.end; - self.read_buffer.with_mut(|rb| rb.reset()); - let increment = window_size_increment.uint31(); - // RFC 9113 §6.9.1: a WINDOW_UPDATE with a flow-control window increment of 0 is an - // error — a stream error (PROTOCOL_ERROR) on a stream, a connection error on stream 0. - if increment == 0 { - if let Some(s) = stream { - // SAFETY: s is *mut Stream from self.streams; valid while the map entry exists - self.end_stream(unsafe { &mut *s }, ErrorCode::PROTOCOL_ERROR); - } else { - self.send_go_away( - 0, - ErrorCode::PROTOCOL_ERROR, - b"WINDOW_UPDATE with 0 increment", - self.last_stream_id.get(), - true, - ); - } - return end; - } - // RFC 9113 §6.9.1: a flow-control window MUST NOT exceed 2^31-1; exceeding it is a - // FLOW_CONTROL_ERROR (stream-scoped on a stream, connection-scoped on stream 0). - if let Some(s) = stream { - // SAFETY: s is *mut Stream from self.streams; valid while the map entry exists - let next = unsafe { (*s).remote_window_size } + increment as u64; - if next > MAX_WINDOW_SIZE as u64 { - // SAFETY: s is *mut Stream from self.streams; valid while the map entry exists - self.end_stream(unsafe { &mut *s }, ErrorCode::FLOW_CONTROL_ERROR); - return end; - } - // SAFETY: s is *mut Stream from self.streams; valid while the map entry exists - unsafe { (*s).remote_window_size = next }; - } else if frame.stream_identifier == 0 { - let next = self.remote_window_size.get() + increment as u64; - if next > MAX_WINDOW_SIZE as u64 { - self.send_go_away( - 0, - ErrorCode::FLOW_CONTROL_ERROR, - b"flow-control window exceeded 2^31-1", - self.last_stream_id.get(), - true, - ); - return end; - } - self.remote_window_size.set(next); + /// A frame callback's JS value that could not be built (allocation + /// failure, a terminating VM): the frame is dropped and the engine stops + /// dispatching this batch; the exception stays pending for `read()` / + /// `on_native_read` (see `Sink::should_stop`). + #[inline] + fn or_stop(&self, built: JsResult) -> Option { + match built { + Ok(v) => Some(v), + Err(_) => { + self.left_exception.set(true); + None } - bun_output::scoped_log!( - H2FrameParser, - "windowSizeIncrement stream {} value {}", - frame.stream_identifier, - window_size_increment.uint31() - ); - // at this point we try to send more data because we received a window update - let _ = self.flush(); - return end; } - // needs more data - data.len() } - /// RFC 9113 §4.1: a frame of unknown/unsupported type MUST be ignored and discarded. - pub(crate) fn handle_unknown_frame(&self, frame: FrameHeader, data: &[u8]) -> usize { - if let Some(content) = self.handle_incomming_payload(data, frame.stream_identifier) { - let end = content.end; - self.read_buffer.with_mut(|rb| rb.reset()); - end - } else { - data.len() + fn string_or_empty_to_js(&self, payload: &[u8]) -> JsResult { + let global = self.handlers.get().global(); + if payload.is_empty() { + return BunString::empty().to_js(&global); } + bun_jsc::bun_string_jsc::create_utf8_for_js(&global, payload) } - /// Handle an inbound PUSH_PROMISE frame (RFC 9113 §6.6). Decode the promised request header - /// block (MUST decode to keep the connection-scoped HPACK context in sync, §4.3), register the - /// promised (even) stream, and dispatch `onStreamPush` so the JS client surfaces it as a pushed - /// stream. A server receiving PUSH_PROMISE is a connection error (§8.4). - pub(crate) fn handle_push_promise_frame( - &self, - frame: FrameHeader, - data: &[u8], - ) -> JsResult { - if self.is_server.get() { - self.send_go_away( - frame.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"Server received PUSH_PROMISE", - self.last_stream_id.get(), - true, - ); - return Ok(data.len()); - } - if let Some(content) = self.handle_incomming_payload(data, frame.stream_identifier) { - let payload = content.data(); - let end = content.end; - self.read_buffer.with_mut(|rb| rb.reset()); - if payload.len() < 4 { - self.send_go_away( - frame.stream_identifier, - ErrorCode::FRAME_SIZE_ERROR, - b"Invalid PUSH_PROMISE frame", - self.last_stream_id.get(), - true, - ); - return Ok(end); - } - let promised = UInt31WithReserved::from_bytes(&payload[0..4]).uint31(); - let global_object = self.handlers.get().global(); - let headers = JSValue::create_empty_array(&global_object, 0)?; - headers.ensure_still_alive(); - let mut sensitive_headers: JSValue = JSValue::UNDEFINED; - let mut off = 4usize; - while off < payload.len() { - let header = match self.decode(&payload[off..]) { - Ok(h) => h, - Err(_) => { - self.send_go_away( - frame.stream_identifier, - ErrorCode::COMPRESSION_ERROR, - b"Invalid HPACK header block", - self.last_stream_id.get(), - true, - ); - return Ok(end); - } - }; - off += header.next; - let js_name = match get_http2_common_string(&global_object, header.well_know as u32) - { - Some(cached) => cached, - None => { - bun_jsc::bun_string_jsc::create_utf8_for_js(&global_object, header.name)? - } - }; - headers.push(&global_object, js_name)?; - headers.push( - &global_object, - bun_jsc::bun_string_jsc::create_utf8_for_js(&global_object, header.value)?, - )?; - if header.never_index { - if sensitive_headers.is_undefined() { - sensitive_headers = JSValue::create_empty_array(&global_object, 0)?; - sensitive_headers.ensure_still_alive(); - } - sensitive_headers.push(&global_object, js_name)?; - } - } - // Register the promised stream so its forthcoming response HEADERS/DATA route to it. - if promised > self.last_stream_id.get() { - self.last_stream_id.set(promised); - if promised > self.last_peer_stream_id.get() { - self.last_peer_stream_id.set(promised); - } - } - let local_window = if self.outstanding_settings.get() > 0 { - DEFAULT_WINDOW_SIZE as u32 - } else { - self.local_settings.get().initial_window_size - }; - let stream = bun_core::heap::into_raw(Box::new(Stream::init( - promised, - local_window, - self.remote_settings - .get() - .map(|s| s.initial_window_size) - .unwrap_or(DEFAULT_WINDOW_SIZE as u32), - self.padding_strategy.get(), - ))); - self.streams.with_mut(|s| s.insert(promised, stream)); - self.dispatch_with_3_extra( - JSH2FrameParser::Gc::onStreamPush, - JSValue::js_number(promised as f64), - headers, - sensitive_headers, - JSValue::js_number(frame.flags as f64), - ); - return Ok(end); + /// Returned *Stream is heap-allocated and stable for the lifetime of this H2FrameParser. + fn handle_received_stream_id(&self, stream_identifier: u32) -> Option<*mut Stream> { + // connection stream + if stream_identifier == 0 { + return None; } - Ok(data.len()) - } - - pub(crate) fn decode_header_block( - &self, - payload: &[u8], - stream: &mut Stream, - flags: u8, - ) -> JsResult> { - bun_output::scoped_log!( - H2FrameParser, - "decodeHeaderBlock isSever: {}", - self.is_server.get() - ); - let mut offset: usize = 0; - let global_object = self.handlers.get().global(); - let stream_id = stream.id; - let headers = JSValue::create_empty_array(&global_object, 0)?; - headers.ensure_still_alive(); + // already exists + if let Some(stream) = self.streams.get().get(&stream_identifier).copied() { + return Some(stream); + } - let mut sensitive_headers: JSValue = JSValue::UNDEFINED; - let mut malformed = false; + if stream_identifier > self.last_stream_id.get() { + self.last_stream_id.set(stream_identifier); + } + let peer_parity: u32 = if self.is_server.get() { 1 } else { 0 }; + if stream_identifier % 2 == peer_parity + && stream_identifier > self.last_peer_stream_id.get() + { + self.last_peer_stream_id.set(stream_identifier); + } - // Stream-level limit violations seen mid-decode. The loop must consume - // the whole block regardless: the HPACK dynamic table is - // connection-scoped, so abandoning the block midway would desync it - // for every other stream. The rejection is applied once after the loop. - let mut rejected = false; - - while offset < payload.len() { - let header = match self.decode(&payload[offset..]) { - Ok(h) => h, - Err(_) => { - // RFC 9113 §4.3: a decoding error in a header block is a - // connection error of type COMPRESSION_ERROR. - self.send_go_away( - stream_id, - ErrorCode::COMPRESSION_ERROR, - b"Invalid HPACK header block", - self.last_stream_id.get(), - true, - ); - return Ok(None); - } - }; - offset += header.next; - bun_output::scoped_log!( - H2FrameParser, - "header {} {}", - BStr::new(header.name), - BStr::new(header.value) - ); - if self.is_server.get() && header.name == b":status" { - self.send_go_away( - stream_id, - ErrorCode::PROTOCOL_ERROR, - b"Server received :status header", - self.last_stream_id.get(), - true, - ); - return Ok(None); - } - - // RFC 7540 Section 6.5.2: Calculate header list size - // Size = name length + value length + HPACK entry overhead per header - stream.header_block_size += - header.name.len() + header.value.len() + HPACK_ENTRY_OVERHEAD; - stream.header_block_count += 1; - - // Check against maxHeaderListSize / maxHeaderListPairs. - if rejected - || stream.header_block_size - > self.local_settings.get().max_header_list_size as usize - || (self.max_header_list_pairs.get() as usize) < stream.header_block_count - { - rejected = true; - continue; - } - - if malformed - || is_malformed_field_name(header.name) - || is_malformed_field_value(header.value) - || (header.name.first() == Some(&b':') - && !if self.is_server.get() { - is_valid_request_pseudo_header(header.name) - } else { - is_valid_response_pseudo_header(header.name) - }) - { - malformed = true; - } else if let Some(js_header_name) = - get_http2_common_string(&global_object, header.well_know as u32) - { - headers.push(&global_object, js_header_name)?; - headers.push( - &global_object, - bun_jsc::bun_string_jsc::create_utf8_for_js(&global_object, header.value)?, - )?; - if header.never_index { - if sensitive_headers.is_undefined() { - sensitive_headers = JSValue::create_empty_array(&global_object, 0)?; - sensitive_headers.ensure_still_alive(); - } - sensitive_headers.push(&global_object, js_header_name)?; - } - } else { - let js_header_name = - bun_jsc::bun_string_jsc::create_utf8_for_js(&global_object, header.name)?; - let js_header_value = - bun_jsc::bun_string_jsc::create_utf8_for_js(&global_object, header.value)?; - - if header.never_index { - if sensitive_headers.is_undefined() { - sensitive_headers = JSValue::create_empty_array(&global_object, 0)?; - sensitive_headers.ensure_still_alive(); - } - sensitive_headers.push(&global_object, js_header_name)?; - } - - headers.push(&global_object, js_header_name)?; - headers.push(&global_object, js_header_value)?; - - js_header_name.ensure_still_alive(); - js_header_value.ensure_still_alive(); - } - } - - if rejected { - self.rejected_streams.set(self.rejected_streams.get() + 1); - if self.max_rejected_streams.get() <= self.rejected_streams.get() { - self.send_go_away( - stream_id, - ErrorCode::ENHANCE_YOUR_CALM, - b"ENHANCE_YOUR_CALM", - self.last_stream_id.get(), - true, - ); - } else { - self.end_stream(stream, ErrorCode::ENHANCE_YOUR_CALM); - } - return Ok(None); - } - - if malformed { - self.end_stream(stream, ErrorCode::PROTOCOL_ERROR); - return Ok(self.streams.get().get(&stream_id).copied()); - } - - self.dispatch_with_3_extra( - JSH2FrameParser::Gc::onStreamHeaders, - stream.get_identifier(), - headers, - sensitive_headers, - JSValue::js_number(flags as f64), - ); - Ok(self.streams.get().get(&stream_id).copied()) - } - - pub(crate) fn handle_data_frame( - &self, - frame: FrameHeader, - data: &[u8], - stream_: Option<*mut Stream>, - ) -> usize { - bun_output::scoped_log!( - H2FrameParser, - "handleDataFrame {} data.len: {}", - if self.is_server.get() { - "server" - } else { - "client" - }, - data.len() - ); - self.read_buffer.with_mut(|rb| rb.reset()); - - let Some(stream_ptr) = stream_ else { - bun_output::scoped_log!( - H2FrameParser, - "received data frame on stream that does not exist" - ); - self.send_go_away( - frame.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"Data frame on connection stream", - self.last_stream_id.get(), - true, - ); - return data.len(); - }; - // 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 mut stream = unsafe { &mut *stream_ptr }; - - let max_frame_size = self.local_settings.get().max_frame_size; - if frame.length > max_frame_size { - bun_output::scoped_log!( - H2FrameParser, - "received data frame with length: {} and max frame size: {}", - frame.length, - max_frame_size - ); - self.send_go_away( - frame.stream_identifier, - ErrorCode::FRAME_SIZE_ERROR, - b"Invalid dataframe frame size", - self.last_stream_id.get(), - true, - ); - return data.len(); - } - - let end: usize = (self.remaining_length.get() as usize).min(data.len()); - let mut payload = &data[0..end]; - // window size considering the full frame.length received so far - self.adjust_window_size(Some(stream), payload.len() as u32); - // SAFETY: stream_ptr unchanged; re-borrow after intervening call (borrowck reshape) - stream = unsafe { &mut *stream_ptr }; - let previous_remaining_length: isize = self.remaining_length.get() as isize; - - self.remaining_length - .set(self.remaining_length.get() - i32::try_from(end).expect("int cast")); - let mut padding: u8 = 0; - let padded = frame.flags & DataFrameFlags::PADDED as u8 != 0; - if padded { - if frame.length < 1 { - // PADDED flag set but no room for the Pad Length octet - self.send_go_away( - frame.stream_identifier, - ErrorCode::FRAME_SIZE_ERROR, - b"Invalid data frame size", - self.last_stream_id.get(), - true, - ); - return data.len(); - } - if let Some(p) = stream.padding { - padding = p; - } else { - if payload.is_empty() { - // await more data because we need to know the padding length - return data.len(); - } - padding = payload[0]; - stream.padding = Some(payload[0]); - } - // RFC 7540 Section 6.1: If the length of the padding is the length of - // the frame payload or greater, the recipient MUST treat this as a - // connection error of type PROTOCOL_ERROR. Validate before computing - // `data_region_end = frame.length - padding` below to avoid underflow. - if padding as usize >= frame.length as usize { - self.send_go_away( - frame.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"Invalid data frame padding", - self.last_stream_id.get(), - true, - ); - return data.len(); - } - } - if self.remaining_length.get() < 0 { - self.send_go_away( - frame.stream_identifier, - ErrorCode::FRAME_SIZE_ERROR, - b"Invalid data frame size", - self.last_stream_id.get(), - true, - ); - return data.len(); - } - let mut emitted = false; - - let start_idx = - (frame.length as usize) - usize::try_from(previous_remaining_length).expect("int cast"); - if start_idx < 1 && padded && !payload.is_empty() { - // Skip the Pad Length octet. Keyed on the PADDED flag rather than - // `padding > 0` because Pad Length = 0 is valid (RFC 7540 Section 6.1) - // and must still be stripped. - payload = &payload[1..]; - } - - if !payload.is_empty() { - // amount of data received so far - let received_size = frame.length as i32 - self.remaining_length.get(); - let data_region_end: usize = frame.length as usize - padding as usize; - let data_region_start: usize = if padded { start_idx.max(1) } else { start_idx }; - let max_payload_size: usize = data_region_end.saturating_sub(data_region_start); - payload = &payload[0..payload.len().min(max_payload_size)]; - bun_output::scoped_log!( - H2FrameParser, - "received_size: {} max_payload_size: {} padding: {} payload.len: {}", - received_size, - max_payload_size, - padding, - payload.len() - ); - - if !payload.is_empty() { - // no padding, just emit the data - let global = self.handlers.get().global(); - // Skip the JS dispatch when conversion fails (VM terminating); keep - // `emitted` so the stream pointer is conservatively re-fetched below. - if let Ok(chunk) = self.handlers.get().binary_type.to_js(payload, &global) { - self.dispatch_with_extra( - JSH2FrameParser::Gc::onStreamData, - stream.get_identifier(), - chunk, - ); - } - emitted = true; - } - } - if self.remaining_length.get() == 0 { - self.current_frame.set(None); - stream.padding = None; - if emitted { - stream = match self.streams.get().get(&frame.stream_identifier).copied() { - // SAFETY: s is *mut Stream from self.streams (heap::alloc); valid while the map entry exists - Some(s) => unsafe { &mut *s }, - None => return end, - }; - } - if frame.flags & DataFrameFlags::END_STREAM as u8 != 0 { - let identifier = stream.get_identifier(); - identifier.ensure_still_alive(); - - if stream.state == StreamState::HALF_CLOSED_LOCAL { - stream.state = StreamState::CLOSED; - stream.free_resources::(self); - } else { - stream.state = StreamState::HALF_CLOSED_REMOTE; - } - self.dispatch_with_extra( - JSH2FrameParser::Gc::onStreamEnd, - identifier, - JSValue::js_number(stream.state as u8 as f64), - ); - } - } - - end - } - - pub(crate) fn handle_go_away_frame( - &self, - frame: FrameHeader, - data: &[u8], - _stream_: Option<*mut Stream>, - ) -> usize { - bun_output::scoped_log!( - H2FrameParser, - "handleGoAwayFrame {} {}", - frame.stream_identifier, - BStr::new(data) - ); - if frame.stream_identifier != 0 { - self.send_go_away( - frame.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"GoAway frame on stream", - self.last_stream_id.get(), - true, - ); - return data.len(); - } - if frame.length < 8 || frame.length > self.local_settings.get().max_frame_size { - self.send_go_away( - frame.stream_identifier, - ErrorCode::FRAME_SIZE_ERROR, - b"invalid GoAway frame size", - self.last_stream_id.get(), - true, - ); - return data.len(); - } - - if let Some(content) = self.handle_incomming_payload(data, frame.stream_identifier) { - let payload = content.data(); - let error_code = u32_from_bytes(&payload[4..8]); - let global = self.handlers.get().global(); - let end = content.end; - self.read_buffer.with_mut(|rb| rb.reset()); - // Skip the JS dispatch when conversion fails (VM terminating). - if let Ok(chunk) = self - .handlers - .get() - .binary_type - .to_js(&payload[8..], &global) - { - self.dispatch_with_2_extra( - JSH2FrameParser::Gc::onGoAway, - JSValue::js_number(error_code as f64), - JSValue::js_number(self.last_stream_id.get() as f64), - chunk, - ); - } - return end; - } - data.len() - } - - /// A frame callback's JS value that could not be built (allocation - /// failure, a terminating VM): the frame is dropped and the engine stops - /// dispatching this batch; the exception stays pending for `read()` / - /// `on_native_read` (see `Sink::should_stop`). - #[inline] - fn or_stop(&self, built: JsResult) -> Option { - match built { - Ok(v) => Some(v), - Err(_) => { - self.left_exception.set(true); - None - } - } - } - - fn string_or_empty_to_js(&self, payload: &[u8]) -> JsResult { - let global = self.handlers.get().global(); - if payload.is_empty() { - return BunString::empty().to_js(&global); - } - bun_jsc::bun_string_jsc::create_utf8_for_js(&global, payload) - } - - pub(crate) fn handle_origin_frame( - &self, - frame: FrameHeader, - data: &[u8], - _: Option<*mut Stream>, - ) -> JsResult { - bun_output::scoped_log!(H2FrameParser, "handleOriginFrame {}", BStr::new(data)); - if self.is_server.get() { - self.send_go_away( - frame.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"ORIGIN frame on server", - self.last_stream_id.get(), - true, - ); - return Ok(data.len()); - } - if frame.stream_identifier != 0 { - self.send_go_away( - frame.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"ORIGIN frame on stream", - self.last_stream_id.get(), - true, - ); - return Ok(data.len()); - } - if let Some(content) = self.handle_incomming_payload(data, frame.stream_identifier) { - let mut payload = content.data(); - let mut origin_value: JSValue = JSValue::UNDEFINED; - let mut count: usize = 0; - let end = content.end; - self.read_buffer.with_mut(|rb| rb.reset()); - - let global = self.handlers.get().global(); - while !payload.is_empty() { - if payload.len() < 2 { - bun_output::scoped_log!( - H2FrameParser, - "error reading ORIGIN frame size: short read" - ); - self.send_go_away( - frame.stream_identifier, - ErrorCode::FRAME_SIZE_ERROR, - b"invalid ORIGIN frame size", - self.last_stream_id.get(), - true, - ); - return Ok(end); - } - let origin_length = u16::from_be_bytes([payload[0], payload[1]]) as usize; - let mut origin_str = &payload[2..]; - if origin_str.len() < origin_length { - self.send_go_away( - frame.stream_identifier, - ErrorCode::FRAME_SIZE_ERROR, - b"invalid ORIGIN frame size", - self.last_stream_id.get(), - true, - ); - return Ok(end); - } - origin_str = &origin_str[0..origin_length]; - if count == 0 { - origin_value = self.string_or_empty_to_js(origin_str)?; - origin_value.ensure_still_alive(); - } else if count == 1 { - // need to create an array - let array = JSValue::create_empty_array(&global, 0)?; - array.ensure_still_alive(); - array.push(&global, origin_value)?; - array.push(&global, self.string_or_empty_to_js(origin_str)?)?; - origin_value = array; - } else { - // we already have an array, just add the origin to it - origin_value.push(&global, self.string_or_empty_to_js(origin_str)?)?; - } - count += 1; - payload = &payload[origin_length + 2..]; - } - - self.dispatch(JSH2FrameParser::Gc::onOrigin, origin_value); - return Ok(end); - } - Ok(data.len()) - } - - pub(crate) fn handle_altsvc_frame( - &self, - frame: FrameHeader, - data: &[u8], - stream_: Option<*mut Stream>, - ) -> JsResult { - bun_output::scoped_log!(H2FrameParser, "handleAltsvcFrame {}", BStr::new(data)); - if self.is_server.get() { - // client should not send ALTSVC frame - self.send_go_away( - frame.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"ALTSVC frame on server", - self.last_stream_id.get(), - true, - ); - return Ok(data.len()); - } - if let Some(content) = self.handle_incomming_payload(data, frame.stream_identifier) { - let payload = content.data(); - let end = content.end; - self.read_buffer.with_mut(|rb| rb.reset()); - - if payload.len() < 2 { - self.send_go_away( - frame.stream_identifier, - ErrorCode::FRAME_SIZE_ERROR, - b"invalid ALTSVC frame size", - self.last_stream_id.get(), - true, - ); - return Ok(end); - } - let origin_length = u16::from_be_bytes([payload[0], payload[1]]) as usize; - let origin_and_value = &payload[2..]; - - if origin_and_value.len() < origin_length { - self.send_go_away( - frame.stream_identifier, - ErrorCode::FRAME_SIZE_ERROR, - b"invalid ALTSVC frame size", - self.last_stream_id.get(), - true, - ); - return Ok(end); - } - if frame.stream_identifier != 0 && stream_.is_none() { - // dont error but stream dont exist so we can ignore it - return Ok(end); - } - - self.dispatch_with_2_extra( - JSH2FrameParser::Gc::onAltSvc, - self.string_or_empty_to_js(&origin_and_value[0..origin_length])?, - self.string_or_empty_to_js(&origin_and_value[origin_length..])?, - JSValue::js_number(frame.stream_identifier as f64), - ); - return Ok(end); - } - Ok(data.len()) - } - - pub(crate) fn handle_rst_stream_frame( - &self, - frame: FrameHeader, - data: &[u8], - stream_: Option<*mut Stream>, - ) -> usize { - bun_output::scoped_log!(H2FrameParser, "handleRSTStreamFrame {}", BStr::new(data)); - let Some(stream_ptr) = stream_ else { - self.send_go_away( - frame.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"RST_STREAM frame on connection stream", - self.last_stream_id.get(), - true, - ); - return data.len(); - }; - // 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 frame.length != 4 { - self.send_go_away( - frame.stream_identifier, - ErrorCode::FRAME_SIZE_ERROR, - b"invalid RST_STREAM frame size", - self.last_stream_id.get(), - true, - ); - return data.len(); - } - - if stream.is_waiting_more_headers { - self.send_go_away( - frame.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"Headers frame without continuation", - self.last_stream_id.get(), - true, - ); - return data.len(); - } - - if let Some(content) = self.handle_incomming_payload(data, frame.stream_identifier) { - let payload = content.data(); - let rst_code = u32_from_bytes(payload); - stream.rst_code = rst_code; - let end = content.end; - self.read_buffer.with_mut(|rb| rb.reset()); - stream.state = StreamState::CLOSED; - let identifier = stream.get_identifier(); - identifier.ensure_still_alive(); - stream.free_resources::(self); - if rst_code == ErrorCode::NO_ERROR.0 { - self.dispatch_with_extra( - JSH2FrameParser::Gc::onStreamEnd, - identifier, - JSValue::js_number(stream.state as u8 as f64), - ); - } else { - self.dispatch_with_extra( - JSH2FrameParser::Gc::onStreamError, - identifier, - JSValue::js_number(rst_code as f64), - ); - } - return end; - } - data.len() - } - - pub(crate) fn handle_ping_frame( - &self, - frame: FrameHeader, - data: &[u8], - _stream_: Option<*mut Stream>, - ) -> usize { - if frame.stream_identifier != 0 { - self.send_go_away( - frame.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"Ping frame on stream", - self.last_stream_id.get(), - true, - ); - return data.len(); - } - - if frame.length != 8 { - self.send_go_away( - frame.stream_identifier, - ErrorCode::FRAME_SIZE_ERROR, - b"Invalid ping frame size", - self.last_stream_id.get(), - true, - ); - return data.len(); - } - - if let Some(content) = self.handle_incomming_payload(data, frame.stream_identifier) { - let payload = content.data(); - let is_not_ack = frame.flags & PingFrameFlags::ACK as u8 == 0; - let end = content.end; - // Note: reset() only clears len so the bytes would stay readable; - // copy out anyway so send_ping/to_js below don't depend on - // that subtlety once read_buffer is mutated further. - let payload_owned = payload.to_vec(); - self.read_buffer.with_mut(|rb| rb.reset()); - - // if is not ACK send response - if is_not_ack { - if self.is_over_session_memory_limit() { - self.send_go_away( - frame.stream_identifier, - ErrorCode::ENHANCE_YOUR_CALM, - b"ENHANCE_YOUR_CALM", - self.last_stream_id.get(), - true, - ); - return end; - } - self.send_ping(true, &payload_owned); - } else { - self.out_standing_pings - .set(self.out_standing_pings.get().saturating_sub(1)); - } - let global = self.handlers.get().global(); - // Skip the JS dispatch when conversion fails (VM terminating). - if let Ok(buffer) = self - .handlers - .get() - .binary_type - .to_js(&payload_owned, &global) - { - self.dispatch_with_extra( - JSH2FrameParser::Gc::onPing, - buffer, - JSValue::from(!is_not_ack), - ); - } - return end; - } - data.len() - } - - pub(crate) fn handle_priority_frame( - &self, - frame: FrameHeader, - data: &[u8], - stream_: Option<*mut Stream>, - ) -> usize { - if frame.length as usize != StreamPriority::BYTE_SIZE { - self.send_go_away( - frame.stream_identifier, - ErrorCode::FRAME_SIZE_ERROR, - b"invalid Priority frame size", - self.last_stream_id.get(), - true, - ); - return data.len(); - } - let Some(stream_ptr) = stream_ else { - if frame.stream_identifier != 0 { - // PRIORITY on an idle/closed stream is permitted (RFC 9113 §5.3.4); ignore it. - if let Some(content) = self.handle_incomming_payload(data, frame.stream_identifier) - { - self.read_buffer.with_mut(|rb| rb.reset()); - return content.end; - } - return data.len(); - } - self.send_go_away( - frame.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"Priority frame on connection stream", - self.last_stream_id.get(), - true, - ); - return data.len(); - }; - // 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 let Some(content) = self.handle_incomming_payload(data, frame.stream_identifier) { - let payload = content.data(); - let end = content.end; - - let mut priority = StreamPriority::default(); - StreamPriority::from(&mut priority, payload); - self.read_buffer.with_mut(|rb| rb.reset()); - - let stream_identifier = UInt31WithReserved::from(priority.stream_identifier); - if stream_identifier.uint31() == stream.id { - self.send_go_away( - stream.id, - ErrorCode::PROTOCOL_ERROR, - b"Priority frame with self dependency", - self.last_stream_id.get(), - true, - ); - return end; - } - stream.stream_dependency = stream_identifier.uint31(); - stream.exclusive = stream_identifier.reserved(); - stream.weight = priority.weight as u16; - - return end; - } - data.len() - } - - /// RFC 7540 Section 6.10: Handle CONTINUATION frame (type=0x9). - pub(crate) fn handle_continuation_frame( - &self, - frame: FrameHeader, - data: &[u8], - stream_: Option<*mut Stream>, - ) -> JsResult { - bun_output::scoped_log!(H2FrameParser, "handleContinuationFrame"); - let Some(stream_ptr) = stream_ else { - self.send_go_away( - frame.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"Continuation on connection stream", - self.last_stream_id.get(), - true, - ); - return Ok(data.len()); - }; - // 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 mut stream = unsafe { &mut *stream_ptr }; - - if !stream.is_waiting_more_headers { - self.send_go_away( - frame.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"Continuation without headers", - self.last_stream_id.get(), - true, - ); - return Ok(data.len()); - } - if frame.length > self.local_settings.get().max_frame_size { - self.send_go_away( - frame.stream_identifier, - ErrorCode::FRAME_SIZE_ERROR, - b"invalid Continuation frame size", - self.last_stream_id.get(), - true, - ); - return Ok(data.len()); - } - if let Some(content) = self.handle_incomming_payload(data, frame.stream_identifier) { - let payload = content.data(); - let end = content.end; - self.read_buffer.with_mut(|rb| rb.reset()); - if stream.pending_header_block.len() + payload.len() - > self.local_settings.get().max_header_list_size as usize - { - // Cap the buffered compressed block at max_header_list_size as a - // DoS bound; the decoded list size is checked separately in - // decode_header_block. - self.send_go_away( - frame.stream_identifier, - ErrorCode::ENHANCE_YOUR_CALM, - b"ENHANCE_YOUR_CALM", - self.last_stream_id.get(), - true, - ); - return Ok(end); - } - stream.pending_header_block.extend_from_slice(payload); - if frame.flags & HeadersFrameFlags::END_HEADERS as u8 == 0 { - // keep buffering until END_HEADERS arrives - return Ok(end); - } - stream.is_waiting_more_headers = false; - self.expecting_continuation.set(0); - // Take ownership of the buffer so re-entrant parser calls from the - // onStreamHeaders dispatch can't alias or free the bytes being decoded. - let block = core::mem::take(&mut stream.pending_header_block); - // Report the original HEADERS frame's flags (plus END_HEADERS now - // that the block is complete), not the CONTINUATION frame's. - let block_flags = stream.pending_header_flags | HeadersFrameFlags::END_HEADERS as u8; - stream = match self.decode_header_block(&block, stream, block_flags)? { - // SAFETY: s is *mut Stream from self.streams (heap::alloc); valid while the map entry exists - Some(s) => unsafe { &mut *s }, - None => return Ok(end), - }; - // END_STREAM finalization was deferred by handle_headers_frame - // until the complete header block had been dispatched. - if stream.end_after_headers { - self.finish_headers_end_stream(stream); - } - return Ok(end); - } - - // needs more data - Ok(data.len()) - } - - /// Finalize a stream whose HEADERS frame carried END_STREAM, after the - /// complete header block has been decoded and dispatched. - fn finish_headers_end_stream(&self, stream: &mut Stream) { - // The stream can be reset (req.close(), AbortSignal) between the - // HEADERS fragment and the CONTINUATION that completes the block; - // don't regress a CLOSED stream or dispatch onStreamEnd after - // onStreamError. - if stream.state == StreamState::CLOSED { - return; - } - let identifier = stream.get_identifier(); - identifier.ensure_still_alive(); - - // no more continuation headers we can call it closed - if stream.state == StreamState::HALF_CLOSED_LOCAL { - stream.state = StreamState::CLOSED; - stream.free_resources::(self); - } else { - stream.state = StreamState::HALF_CLOSED_REMOTE; - } - self.dispatch_with_extra( - JSH2FrameParser::Gc::onStreamEnd, - identifier, - JSValue::js_number(stream.state as u8 as f64), - ); - } - - pub(crate) fn handle_headers_frame( - &self, - frame: FrameHeader, - data: &[u8], - stream_: Option<*mut Stream>, - ) -> JsResult { - bun_output::scoped_log!( - H2FrameParser, - "handleHeadersFrame {}", - if self.is_server.get() { - "server" - } else { - "client" - } - ); - let Some(stream_ptr) = stream_ else { - self.send_go_away( - frame.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"Headers frame on connection stream", - self.last_stream_id.get(), - true, - ); - return Ok(data.len()); - }; - // 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 mut stream = unsafe { &mut *stream_ptr }; - - if frame.length > self.local_settings.get().max_frame_size { - self.send_go_away( - frame.stream_identifier, - ErrorCode::FRAME_SIZE_ERROR, - b"invalid Headers frame size", - self.last_stream_id.get(), - true, - ); - return Ok(data.len()); - } - - if stream.is_waiting_more_headers { - self.send_go_away( - frame.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"Headers frame without continuation", - self.last_stream_id.get(), - true, - ); - return Ok(data.len()); - } - - if let Some(content) = self.handle_incomming_payload(data, frame.stream_identifier) { - let payload = content.data(); - let mut offset: usize = 0; - let mut padding: usize = 0; - let end_ = content.end; - self.read_buffer.with_mut(|rb| rb.reset()); - - if frame.flags & HeadersFrameFlags::PADDED as u8 != 0 { - if payload.len() < 1 { - self.send_go_away( - frame.stream_identifier, - ErrorCode::FRAME_SIZE_ERROR, - b"invalid Headers frame size", - self.last_stream_id.get(), - true, - ); - return Ok(end_); - } - // padding length - padding = payload[0] as usize; - offset += 1; - } - if frame.flags & HeadersFrameFlags::PRIORITY as u8 != 0 { - // skip priority (client dont need to care about it) - offset += 5; - } - if offset > payload.len() { - self.send_go_away( - frame.stream_identifier, - ErrorCode::FRAME_SIZE_ERROR, - b"invalid Headers frame size", - self.last_stream_id.get(), - true, - ); - return Ok(end_); - } - if padding > payload.len() - offset { - self.send_go_away( - frame.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"invalid Headers frame padding", - self.last_stream_id.get(), - true, - ); - return Ok(end_); - } - let end = payload.len() - padding; - stream.end_after_headers = frame.flags & HeadersFrameFlags::END_STREAM as u8 != 0; - stream.header_block_size = 0; - stream.header_block_count = 0; - stream.pending_header_block.clear(); - stream.is_waiting_more_headers = - frame.flags & HeadersFrameFlags::END_HEADERS as u8 == 0; - if stream.is_waiting_more_headers { - // Buffer fragments until END_HEADERS (RFC 9113 §4.3); the block is - // decoded and END_STREAM finalized in handle_continuation_frame so - // the JS event order stays onStreamHeaders -> onStreamEnd. - let fragment = &payload[offset..end]; - if fragment.len() > self.local_settings.get().max_header_list_size as usize { - // Cap the buffered compressed block at max_header_list_size - // as a DoS bound; the decoded list size is checked separately - // in decode_header_block. - self.send_go_away( - frame.stream_identifier, - ErrorCode::ENHANCE_YOUR_CALM, - b"ENHANCE_YOUR_CALM", - self.last_stream_id.get(), - true, - ); - return Ok(end_); - } - stream.pending_header_block.extend_from_slice(fragment); - stream.pending_header_flags = frame.flags; - self.expecting_continuation.set(frame.stream_identifier); - return Ok(end_); - } - stream = match self.decode_header_block(&payload[offset..end], stream, frame.flags)? { - // SAFETY: s is *mut Stream from self.streams (heap::alloc); valid while the map entry exists - Some(s) => unsafe { &mut *s }, - None => return Ok(end_), - }; - if stream.end_after_headers { - self.finish_headers_end_stream(stream); - } - return Ok(end_); - } - - // needs more data - Ok(data.len()) - } - - pub(crate) fn handle_settings_frame(&self, frame: FrameHeader, data: &[u8]) -> usize { - let is_ack = frame.flags & SettingsFlags::ACK as u8 != 0; - - bun_output::scoped_log!( - H2FrameParser, - "handleSettingsFrame {} isACK {}", - if self.is_server.get() { - "server" - } else { - "client" - }, - is_ack - ); - if frame.stream_identifier != 0 { - self.send_go_away( - frame.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"Settings frame on connection stream", - self.last_stream_id.get(), - true, - ); - return data.len(); - } - // defer if (!isACK) this.sendSettingsACK(); - let send_ack_on_exit = !is_ack; - - let setting_byte_size = SettingsPayloadUnit::BYTE_SIZE; - if frame.length > 0 { - if is_ack || !(frame.length as usize).is_multiple_of(setting_byte_size) { - bun_output::scoped_log!(H2FrameParser, "invalid settings frame size"); - self.send_go_away( - frame.stream_identifier, - ErrorCode::FRAME_SIZE_ERROR, - b"Invalid settings frame size", - self.last_stream_id.get(), - true, - ); - if send_ack_on_exit { - self.send_settings_ack(); - } - return data.len(); - } - } else { - if is_ack { - // we received an ACK - bun_output::scoped_log!(H2FrameParser, "settings frame ACK"); - - // we can now write any request - if self.outstanding_settings.get() > 0 { - self.outstanding_settings - .set(self.outstanding_settings.get() - 1); - - // Per RFC 7540 Section 6.9.2: When INITIAL_WINDOW_SIZE changes, adjust - // all existing stream windows by the difference. - if self.outstanding_settings.get() == 0 - && self.local_settings.get().initial_window_size as u64 - != DEFAULT_WINDOW_SIZE - { - let old_size: i64 = DEFAULT_WINDOW_SIZE as i64; - let new_size: i64 = self.local_settings.get().initial_window_size as i64; - let delta = new_size - old_size; - for (_, item) in self.streams.get().iter() { - // SAFETY: item is &*mut Stream from streams.iter(); the boxed Stream outlives the iteration - let stream = unsafe { &mut **item }; - if delta >= 0 { - stream.window_size = stream - .window_size - .saturating_add(u64::try_from(delta).expect("int cast")); - } else { - stream.window_size = stream - .window_size - .saturating_sub(u64::try_from(-delta).expect("int cast")); - } - } - bun_output::scoped_log!( - H2FrameParser, - "adjusted stream windows by delta {} (old: {}, new: {})", - delta, - old_size, - new_size - ); - } - } - - let global = self.handlers.get().global(); - self.dispatch( - JSH2FrameParser::Gc::onLocalSettings, - self.local_settings.get().to_js(&global), - ); - } else { - bun_output::scoped_log!( - H2FrameParser, - "empty settings has remoteSettings? {}", - self.remote_settings.get().is_some() - ); - if self.remote_settings.get().is_none() { - // ok empty settings so default settings - let remote_settings = FullSettingsPayload::default(); - self.remote_settings.set(Some(remote_settings)); - let _iws = remote_settings.initial_window_size; - bun_output::scoped_log!( - H2FrameParser, - "remoteSettings.initialWindowSize: {} {} {}", - _iws, - self.remote_used_window_size.get(), - self.remote_window_size.get() - ); - - if remote_settings.initial_window_size as u64 >= self.remote_window_size.get() { - for (_, item) in self.streams.get().iter() { - // SAFETY: item is &*mut Stream from streams.iter(); the boxed Stream outlives the iteration - let stream = unsafe { &mut **item }; - if remote_settings.initial_window_size as u64 - >= stream.remote_window_size - { - stream.remote_window_size = - remote_settings.initial_window_size as u64; - } - } - } - let global = self.handlers.get().global(); - self.dispatch( - JSH2FrameParser::Gc::onRemoteSettings, - remote_settings.to_js(&global), - ); - } - // defer chain (reverse order) - self.increment_window_size_if_needed(); - let _ = self.flush(); - } - - self.current_frame.set(None); - if send_ack_on_exit { - self.send_settings_ack(); - } - return 0; - } - if let Some(content) = self.handle_incomming_payload(data, frame.stream_identifier) { - let mut remote_settings: FullSettingsPayload = - self.remote_settings.get().unwrap_or_default(); - let mut i: usize = 0; - let payload = content.data(); - let end = content.end; - while i < payload.len() { - let mut unit = SettingsPayloadUnit::default(); - SettingsPayloadUnit::from::(&mut unit, &payload[i..i + setting_byte_size], 0); - if SettingsType(unit.type_) == SettingsType::SETTINGS_MAX_FRAME_SIZE - && (unit.value < 16384 || unit.value > MAX_FRAME_SIZE) - { - self.read_buffer.with_mut(|rb| rb.reset()); - self.send_go_away( - frame.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"Invalid SETTINGS_MAX_FRAME_SIZE", - self.last_stream_id.get(), - true, - ); - return end; - } - // RFC 9113 §6.5.2: SETTINGS_ENABLE_PUSH / SETTINGS_ENABLE_CONNECT_PROTOCOL must be - // 0 or 1, otherwise a connection error of type PROTOCOL_ERROR. - if (SettingsType(unit.type_) == SettingsType::SETTINGS_ENABLE_PUSH - || SettingsType(unit.type_) == SettingsType::SETTINGS_ENABLE_CONNECT_PROTOCOL) - && unit.value > 1 - { - self.read_buffer.with_mut(|rb| rb.reset()); - self.send_go_away( - frame.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"Invalid SETTINGS value", - self.last_stream_id.get(), - true, - ); - return end; - } - // RFC 9113 §6.5.2: SETTINGS_INITIAL_WINDOW_SIZE above 2^31-1 is a FLOW_CONTROL_ERROR. - if SettingsType(unit.type_) == SettingsType::SETTINGS_INITIAL_WINDOW_SIZE - && unit.value > MAX_WINDOW_SIZE - { - self.read_buffer.with_mut(|rb| rb.reset()); - self.send_go_away( - frame.stream_identifier, - ErrorCode::FLOW_CONTROL_ERROR, - b"Invalid SETTINGS_INITIAL_WINDOW_SIZE", - self.last_stream_id.get(), - true, - ); - return end; - } - remote_settings.update_with(unit); - let (_ut, _uv) = (unit.type_, unit.value); - bun_output::scoped_log!( - H2FrameParser, - "remoteSettings: {} {} isServer: {}", - _ut, - _uv, - self.is_server.get() - ); - i += setting_byte_size; - } - self.read_buffer.with_mut(|rb| rb.reset()); - self.remote_settings.set(Some(remote_settings)); - let _iws = remote_settings.initial_window_size; - bun_output::scoped_log!( - H2FrameParser, - "remoteSettings.initialWindowSize: {} {} {}", - _iws, - self.remote_used_window_size.get(), - self.remote_window_size.get() - ); - if remote_settings.initial_window_size as u64 >= self.remote_window_size.get() { - for (_, item) in self.streams.get().iter() { - // SAFETY: item is &*mut Stream from streams.iter(); the boxed Stream outlives the iteration - let stream = unsafe { &mut **item }; - if remote_settings.initial_window_size as u64 >= stream.remote_window_size { - stream.remote_window_size = remote_settings.initial_window_size as u64; - } - } - } - let global = self.handlers.get().global(); - self.dispatch( - JSH2FrameParser::Gc::onRemoteSettings, - remote_settings.to_js(&global), - ); - // defer chain - self.increment_window_size_if_needed(); - let _ = self.flush(); - if send_ack_on_exit { - self.send_settings_ack(); - } - return end; - } - // needs more data - if send_ack_on_exit { - self.send_settings_ack(); - } - data.len() - } - - /// Returned *Stream is heap-allocated and stable for the lifetime of this H2FrameParser. - fn handle_received_stream_id(&self, stream_identifier: u32) -> Option<*mut Stream> { - // connection stream - if stream_identifier == 0 { - return None; - } - - // already exists - if let Some(stream) = self.streams.get().get(&stream_identifier).copied() { - return Some(stream); - } - - if stream_identifier > self.last_stream_id.get() { - self.last_stream_id.set(stream_identifier); - } - let peer_parity: u32 = if self.is_server.get() { 1 } else { 0 }; - if stream_identifier % 2 == peer_parity - && stream_identifier > self.last_peer_stream_id.get() - { - self.last_peer_stream_id.set(stream_identifier); - } - - // new stream open - let local_window_size = if self.outstanding_settings.get() > 0 { - DEFAULT_WINDOW_SIZE as u32 - } else { - self.local_settings.get().initial_window_size - }; - let stream = bun_core::heap::into_raw(Box::new(Stream::init( - stream_identifier, - local_window_size, - self.remote_settings - .get() - .map(|s| s.initial_window_size) - .unwrap_or(DEFAULT_WINDOW_SIZE as u32), - self.padding_strategy.get(), - ))); - self.streams - .with_mut(|s| s.insert(stream_identifier, stream)); + // new stream open + let local_window_size = if self.outstanding_settings.get() > 0 { + DEFAULT_WINDOW_SIZE as u32 + } else { + self.local_settings.get().initial_window_size + }; + let stream = bun_core::heap::into_raw(Box::new(Stream::init( + stream_identifier, + local_window_size, + self.remote_settings + .get() + .map(|s| s.initial_window_size) + .unwrap_or(DEFAULT_WINDOW_SIZE as u32), + self.padding_strategy.get(), + ))); + self.streams + .with_mut(|s| s.insert(stream_identifier, stream)); let Some(this_value) = self.strong_this.get().try_get() else { return Some(stream); @@ -5411,257 +3523,6 @@ impl H2FrameParser { Some(stream) } - /// Stream lookup for inbound frames. Only a HEADERS frame may allocate new - /// stream state (RFC 9113 §5.1); any other frame type referencing an - /// unknown stream id is treated as idle/closed and does not allocate. - fn lookup_inbound_stream(&self, stream_identifier: u32, frame_type: u8) -> Option<*mut Stream> { - if stream_identifier == 0 { - return None; - } - if let Some(stream) = self.streams.get().get(&stream_identifier).copied() { - return Some(stream); - } - if frame_type != FrameType::HTTP_FRAME_HEADERS as u8 || !self.is_server.get() { - return None; - } - // RFC 9113 §4.3: while a header block is mid-reassembly the only legal - // frame is a CONTINUATION for that stream, and dispatch_frame will - // reject this one. Don't allocate stream state, bump last_stream_id, - // or fire onStreamStart for a frame that is about to be rejected. - if self.expecting_continuation.get() != 0 { - return None; - } - // Client-initiated streams must use odd identifiers (RFC 9113 §5.1.1). - if stream_identifier & 1 == 0 { - return None; - } - // Bound per-connection stream state before allocating: a peer flooding - // tiny HEADERS frames with fresh stream ids would otherwise grow - // `streams` (and the JS objects pinned by `streamStart`) without limit. - // Mirrors the maxSessionMemory check on the PING and request() paths. - if self.is_over_session_memory_limit() { - self.send_go_away( - stream_identifier, - ErrorCode::ENHANCE_YOUR_CALM, - b"ENHANCE_YOUR_CALM", - self.last_stream_id.get(), - true, - ); - return None; - } - self.handle_received_stream_id(stream_identifier) - } - - fn read_bytes(&self, bytes: &[u8]) -> JsResult { - // read() loops this per frame. A prior frame's callback can drain - // microtasks that tear the worker down (worker.terminate()), leaving a - // pending (termination) exception; dispatching further frames then calls - // JS with that exception pending (assertNoException) or with torn-down - // values. Stop consuming once an exception is pending. - if self.handlers.get().global().has_exception() { - return Ok(bytes.len()); - } - bun_output::scoped_log!(H2FrameParser, "read {}", bytes.len()); - if self.is_server.get() && self.preface_received_len.get() < 24 { - // Handle Server Preface - let preface_missing: usize = 24 - self.preface_received_len.get() as usize; - let preface_available = preface_missing.min(bytes.len()); - let expected = &b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"[self.preface_received_len.get() - as usize - ..preface_available + self.preface_received_len.get() as usize]; - if bytes[0..preface_available] != *expected { - // invalid preface - bun_output::scoped_log!(H2FrameParser, "invalid preface"); - self.send_go_away( - 0, - ErrorCode::PROTOCOL_ERROR, - b"Invalid preface", - self.last_stream_id.get(), - true, - ); - return Ok(preface_available); - } - self.preface_received_len.set( - self.preface_received_len.get() - + u8::try_from(preface_available).expect("int cast"), - ); - return Ok(preface_available); - } - if let Some(header) = self.current_frame.get() { - bun_output::scoped_log!( - H2FrameParser, - "current frame {} {} {} {} {}", - if self.is_server.get() { - "server" - } else { - "client" - }, - header.type_, - header.length, - header.flags, - header.stream_identifier - ); - - let stream = self.lookup_inbound_stream(header.stream_identifier, header.type_); - return self.dispatch_frame(header, bytes, stream, 0); - } - - // nothing to do - if bytes.is_empty() { - return Ok(bytes.len()); - } - - let buffered_data = self.read_buffer.get().list.len(); - - // we can have less than 9 bytes buffered - if buffered_data > 0 { - let total = buffered_data + bytes.len(); - if total < FrameHeader::BYTE_SIZE { - // buffer more data - let _ = self.read_buffer.with_mut(|rb| rb.append_slice(bytes)); - self.global() - .vm() - .deprecated_report_extra_memory(bytes.len()); - return Ok(bytes.len()); - } - // Reassemble the 9 wire bytes on the stack and decode in one shot - // — no shared scratch state. - let needed = FrameHeader::BYTE_SIZE - buffered_data; - let mut raw = [0u8; FrameHeader::BYTE_SIZE]; - raw[..buffered_data].copy_from_slice(&self.read_buffer.get().list[..buffered_data]); - raw[buffered_data..].copy_from_slice(&bytes[..needed]); - let mut header = FrameHeader::decode(&raw); - // ignore the reserved bit - let id = UInt31WithReserved::from(header.stream_identifier); - header.stream_identifier = id.uint31(); - // reset for later use - self.read_buffer.with_mut(|rb| rb.reset()); - - self.current_frame.set(Some(header)); - self.remaining_length.set(header.length as i32); - bun_output::scoped_log!( - H2FrameParser, - "new frame {} {} {} {}", - header.type_, - header.length, - header.flags, - header.stream_identifier - ); - let stream = self.lookup_inbound_stream(header.stream_identifier, header.type_); - - return self.dispatch_frame(header, &bytes[needed..], stream, needed); - } - - if bytes.len() < FrameHeader::BYTE_SIZE { - // buffer more dheaderata - let _ = self.read_buffer.with_mut(|rb| rb.append_slice(bytes)); - self.global() - .vm() - .deprecated_report_extra_memory(bytes.len()); - return Ok(bytes.len()); - } - - let header = FrameHeader::decode( - bytes[..FrameHeader::BYTE_SIZE] - .try_into() - .expect("infallible: size matches"), - ); - - bun_output::scoped_log!( - H2FrameParser, - "new frame {} {} {} {} {}", - if self.is_server.get() { - "server" - } else { - "client" - }, - header.type_, - header.length, - header.flags, - header.stream_identifier - ); - self.current_frame.set(Some(header)); - self.remaining_length.set(header.length as i32); - let stream = self.lookup_inbound_stream(header.stream_identifier, header.type_); - self.dispatch_frame( - header, - &bytes[FrameHeader::BYTE_SIZE..], - stream, - FrameHeader::BYTE_SIZE, - ) - } - - // Note: hoisted from three identical switch blocks in read_bytes for borrowck/DRY. - // The `add` parameter is the number of bytes already consumed before `bytes` (0, `needed`, or BYTE_SIZE). - fn dispatch_frame( - &self, - header: FrameHeader, - bytes: &[u8], - stream: Option<*mut Stream>, - add: usize, - ) -> JsResult { - // RFC 9113 §4.3 / §6.10: once a HEADERS frame without END_HEADERS has - // been received, the only frame permitted on the connection is a - // CONTINUATION for that same stream until the header block is complete. - let expecting = self.expecting_continuation.get(); - if expecting != 0 - && (header.type_ != FrameType::HTTP_FRAME_CONTINUATION as u8 - || header.stream_identifier != expecting) - { - self.send_go_away( - header.stream_identifier, - ErrorCode::PROTOCOL_ERROR, - b"Expected CONTINUATION frame", - self.last_stream_id.get(), - true, - ); - return Ok(bytes.len() + add); - } - Ok(match header.type_ { - x if x == FrameType::HTTP_FRAME_SETTINGS as u8 => { - self.handle_settings_frame(header, bytes) + add - } - x if x == FrameType::HTTP_FRAME_WINDOW_UPDATE as u8 => { - self.handle_window_update_frame(header, bytes, stream) + add - } - x if x == FrameType::HTTP_FRAME_HEADERS as u8 => { - self.handle_headers_frame(header, bytes, stream)? + add - } - x if x == FrameType::HTTP_FRAME_DATA as u8 => { - self.handle_data_frame(header, bytes, stream) + add - } - x if x == FrameType::HTTP_FRAME_CONTINUATION as u8 => { - self.handle_continuation_frame(header, bytes, stream)? + add - } - x if x == FrameType::HTTP_FRAME_PRIORITY as u8 => { - self.handle_priority_frame(header, bytes, stream) + add - } - x if x == FrameType::HTTP_FRAME_PING as u8 => { - self.handle_ping_frame(header, bytes, stream) + add - } - x if x == FrameType::HTTP_FRAME_GOAWAY as u8 => { - self.handle_go_away_frame(header, bytes, stream) + add - } - x if x == FrameType::HTTP_FRAME_RST_STREAM as u8 => { - self.handle_rst_stream_frame(header, bytes, stream) + add - } - x if x == FrameType::HTTP_FRAME_ALTSVC as u8 => { - self.handle_altsvc_frame(header, bytes, stream)? + add - } - x if x == FrameType::HTTP_FRAME_ORIGIN as u8 => { - self.handle_origin_frame(header, bytes, stream)? + add - } - 0x05 => { - // PUSH_PROMISE (0x5): not a FrameType variant. Surface the pushed stream to JS. - self.handle_push_promise_frame(header, bytes)? + add - } - _ => { - // RFC 9113 §4.1: frames of unknown/unsupported type MUST be ignored and discarded. - self.handle_unknown_frame(header, bytes) + add - } - }) - } - fn to_writer(&self) -> DirectWriterStruct { DirectWriterStruct { writer: bun_ptr::BackRef::new(self), @@ -5679,7 +3540,6 @@ fn rewrite_settings_to_js(s: &crate::api::h2::settings::Settings, global: Global max_frame_size: s.max_frame_size, max_header_list_size: s.max_header_list_size, enable_connect_protocol: s.enable_connect_protocol, - ..Default::default() }; fp.to_js(&global) } @@ -6022,7 +3882,6 @@ impl crate::api::h2::connection::Sink for H2FrameParser { max_frame_size: settings.max_frame_size, max_header_list_size: settings.max_header_list_size, enable_connect_protocol: settings.enable_connect_protocol, - ..Default::default() }; self.remote_settings.set(Some(fp)); // §6.9.2 (mirrors the legacy inbound): when the peer's INITIAL_WINDOW_SIZE grows, raise the @@ -9844,9 +7703,6 @@ impl H2FrameParser { FullSettingsPayload::default().max_header_list_size, ), remote_settings: Cell::new(None), - current_frame: Cell::new(None), - remaining_length: Cell::new(0), - read_buffer: JsCell::new(MutableString::default()), window_size: Cell::new(DEFAULT_WINDOW_SIZE), used_window_size: Cell::new(0), remote_window_size: Cell::new(DEFAULT_WINDOW_SIZE), @@ -9872,10 +7728,8 @@ impl H2FrameParser { strict_single_value_fields: Cell::new(true), last_stream_id: Cell::new(0), last_peer_stream_id: Cell::new(0), - expecting_continuation: Cell::new(0), is_server: Cell::new(false), left_exception: Cell::new(false), - preface_received_len: Cell::new(0), write_buffer: JsCell::new(Vec::::default()), write_buffer_offset: Cell::new(0), outbound_queue_size: Cell::new(0), @@ -10105,10 +7959,8 @@ impl H2FrameParser { self.unregister_auto_flush(); self.detach_native_socket(); - // Free the allocation, not just the length: `reset()` would only - // clear `len`; detach() is reachable from JS without a following `deinit`, so the - // capacity must be released here. Drop-and-replace = free. - self.read_buffer.set(MutableString::default()); + // Free the allocation, not just the length: detach() is reachable from JS without a + // following `deinit`, so the capacity must be released here. self.write_buffer.with_mut(|wb| wb.clear_and_free()); self.tx_tracker.set(TxFrameTracker::default()); // Drop every per-stream JS context root; the parser is detaching. @@ -10167,7 +8019,7 @@ impl H2FrameParser { drop(streams); // Drop is still owed on the remaining fields (`handlers`, `auto_flusher`, the now- - // empty `streams`/`read_buffer`/`write_buffer`/`strong_this`, …); + // empty `streams`/`write_buffer`/`strong_this`, …); // `HiveArrayFallback::put` runs `drop_in_place` before recycling the slot, // and `heap::destroy` drops via `Box`, so both branches drop exactly once. // R-2: refcount==0, sole owner — `as_ctx_ptr()` is sound for the diff --git a/test/internal/source-lints/dead-symbols-ffi-sys-test-runner-cpp.test.ts b/test/internal/source-lints/dead-symbols-ffi-sys-test-runner-cpp.test.ts index 794217741009..2c16966759d9 100644 --- a/test/internal/source-lints/dead-symbols-ffi-sys-test-runner-cpp.test.ts +++ b/test/internal/source-lints/dead-symbols-ffi-sys-test-runner-cpp.test.ts @@ -189,7 +189,6 @@ test("dead C++ bindings do not reappear", () => { ], ["src/jsc/bindings/webcore/streams/StreamQueue.h", /\bsetTotalSize\b/], ["src/jsc/bindings/webcore/WorkerMessagingProxy.h", /\baskedToTerminate\(\)|\bloaderContextIdentifier\(\)/], - ["src/jsc/bindings/BunHttp2CommonStrings.h", /\bcommonStringInitializer\b/], ["src/jsc/modules/NativeModuleList.h", /\bBUN_FOREACH_CJS_NATIVE_MODULE\b/], // Iso-subspace slots no class ever allocated from. [ From 77a83503907f136be70b5e55a1d2ad1a72adca8e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:09:23 +0000 Subject: [PATCH 2/6] Remove unreferenced native bindings, the write-only watcher loader column, and dead helpers in bun_core, bun_sys, bun_jsc, tcc_sys and builtins.d.ts --- src/bun_core/lib.rs | 10 -- src/bun_core/util.rs | 27 +----- src/bundler/bundle_v2.rs | 5 +- src/js/builtins.d.ts | 131 -------------------------- src/jsc/JSValue.rs | 6 -- src/jsc/RuntimeTranspilerStore.rs | 2 - src/jsc/VirtualMachine.rs | 4 +- src/jsc/hot_reloader.rs | 27 +----- src/runtime/cli/test_command.rs | 3 +- src/runtime/jsc_hooks.rs | 18 +--- src/runtime/node/node.classes.ts | 5 - src/runtime/node/node_fs.rs | 10 -- src/runtime/node/node_fs_binding.rs | 23 ----- src/runtime/node/node_fs_watcher.rs | 9 -- src/runtime/node/quic/endpoint.rs | 7 -- src/runtime/node/quic/quic.classes.ts | 8 -- src/runtime/node/quic/session.rs | 9 -- src/sys/lib.rs | 22 +---- src/sys/linux_syscall.rs | 2 +- src/tcc_sys/lib.rs | 3 +- src/tcc_sys/tcc.rs | 3 - src/watcher/Watcher.rs | 13 +-- src/watcher/lib.rs | 19 ---- 23 files changed, 17 insertions(+), 349 deletions(-) diff --git a/src/bun_core/lib.rs b/src/bun_core/lib.rs index f53e3a38fcd4..178a3ea5b94d 100644 --- a/src/bun_core/lib.rs +++ b/src/bun_core/lib.rs @@ -130,16 +130,6 @@ impl RawSlice { pub const fn new(s: &[T]) -> Self { RawSlice(core::ptr::from_ref(s)) } - /// Wrap a raw slice pointer. - /// - /// # Safety - /// `p` must either be a (dangling, len 0) empty slice or point to `len` - /// initialized `T` that remain live and stable for the lifetime of every - /// `RawSlice` copied from the result. - #[inline] - pub const unsafe fn from_raw(p: *const [T]) -> Self { - RawSlice(p) - } #[inline] pub const fn as_ptr(self) -> *const [T] { self.0 diff --git a/src/bun_core/util.rs b/src/bun_core/util.rs index 3908c9d40229..8a599cc75379 100644 --- a/src/bun_core/util.rs +++ b/src/bun_core/util.rs @@ -37,11 +37,6 @@ pub unsafe fn bytes_as_slice_mut(bytes: &mut [u8]) -> &mut [T] { pub struct Unaligned(T); impl Unaligned { - #[inline(always)] - pub const fn new(value: T) -> Self { - Self(value) - } - #[inline(always)] pub fn get(self) -> T { // `self` is by-value (already moved into an aligned local), so a plain @@ -601,8 +596,7 @@ macro_rules! opaque_extern { // `bun_threading::Guarded` / `bun_threading::RwLock` directly. // // API parity with the previous `parking_lot` aliases: `const fn new(T)`, -// `.lock()` → guard (no `Result`), `.try_lock()` → `Option`, `.get_mut()`, -// `Default`. +// `.lock()` → guard (no `Result`), `.try_lock()` → `Option`, `Default`. /// Poison-free `std::sync::Mutex` wrapper. See module note above for why /// this is not `bun_threading::Guarded`. @@ -635,13 +629,6 @@ impl Mutex { Err(std::sync::TryLockError::WouldBlock) => None, } } - - #[inline] - pub fn get_mut(&mut self) -> &mut T { - self.0 - .get_mut() - .unwrap_or_else(std::sync::PoisonError::into_inner) - } } impl Default for Mutex { @@ -675,13 +662,6 @@ impl RwLock { .write() .unwrap_or_else(std::sync::PoisonError::into_inner) } - - #[inline] - pub fn get_mut(&mut self) -> &mut T { - self.0 - .get_mut() - .unwrap_or_else(std::sync::PoisonError::into_inner) - } } impl Default for RwLock { @@ -4922,11 +4902,6 @@ impl Timespec { const NS_PER_S: i64 = crate::time::NS_PER_S as i64; const NS_PER_MS: i64 = crate::time::NS_PER_MS as i64; - #[inline] - pub const fn new(sec: i64, nsec: i64) -> Self { - Self { sec, nsec } - } - #[inline] pub fn eql(&self, other: &Timespec) -> bool { self == other diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 24999fd82b68..5d0e56ae335f 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -4541,7 +4541,6 @@ pub mod bv2_impl { fd, &load.path, bun_wyhash::hash(load.path.as_ref()) as u32, - bun_watcher::Loader(code.loader as u8), bun_sys::Fd::INVALID, None, ), @@ -6967,12 +6966,11 @@ pub mod bv2_impl { if this.bun_watcher.is_some() { if parse_result.watcher_data.fd != bun_sys::Fd::INVALID { let source_index = parse_result.value.source_index(); - // borrowck — read source path/loader before + // borrowck — read the source path before // `should_add_watcher(&self)` so the column borrow is released. let source_path = this.graph.input_files.items_source()[source_index as usize] .path .text; - let loader = this.graph.input_files.items_loader()[source_index as usize]; if this.should_add_watcher(source_path) { // const generic `CLONE_FILE_PATH = isWindows` // matches `cfg!(windows)` at compile time. @@ -6983,7 +6981,6 @@ pub mod bv2_impl { parse_result.watcher_data.fd, source_path, bun_wyhash::hash(source_path) as u32, - bun_watcher::Loader(loader as u8), parse_result.watcher_data.dir_fd, None, ); diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index 8c65bffa7a64..d917bc2f11aa 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -82,12 +82,6 @@ declare function $argumentCount(): number; /** array.push(item) */ declare function $arrayPush(array: T[], item: T): void; -/** - * gets a property on an object - */ -declare function $getByValWithThis(target: any, receiver: any, propertyKey: string): void; -/** gets the prototype of an object */ -declare function $getPrototypeOf(value: any): any; /** * Returns the internal Promise state as a small integer: * `0` = pending, `1` = fulfilled, `2` = rejected. @@ -119,23 +113,6 @@ declare function $getInternalField number: N, ): Fields[N]; /** - * Use {@link $fulfillPromise} when: - * - Fulfilling with primitive values (numbers, strings, booleans, null, undefined) - * - Fulfilling with plain objects that definitely don't have a then method - * - You're in internal code that has already done the thenable checking - * - * Use {@link $resolvePromise} when: - * - The value might be a promise or thenable - * - You need the full resolution algorithm (self-check, thenable unwrapping) - * - You're implementing user-facing APIs where the resolution value is unknown - */ -declare function $fulfillPromise(promise: Promise, value: NoInfer): void; -/** - * Use {@link $fulfillPromise} when: - * - Fulfilling with primitive values (numbers, strings, booleans, null, undefined) - * - Fulfilling with plain objects that definitely don't have a then method - * - You're in internal code that has already done the thenable checking - * * Use {@link $resolvePromise} when: * - The value might be a promise or thenable * - You need the full resolution algorithm (self-check, thenable unwrapping) @@ -148,13 +125,7 @@ declare function $resolvePromise(promise: Promise, value: NoInfer): voi declare function $rejectPromise(promise: Promise, value: unknown): void; declare function $loadEsmIntoCjs(...args: any[]): TODO; -declare function $getGeneratorInternalField(): TODO; -declare function $getAsyncGeneratorInternalField(): TODO; -declare function $getAbstractModuleRecordInternalField(): TODO; -declare function $getArrayIteratorInternalField(): TODO; -declare function $getStringIteratorInternalField(): TODO; declare function $getProxyInternalField(): TODO; -declare function $idWithProfile(): TODO; /** * True for object-like `JSCell`s. That is, this is roughly equivalent to this * JS code: @@ -172,18 +143,11 @@ declare function $isObject(obj: unknown): obj is object; declare function $isArray(obj: T): obj is Extract | Extract; declare function $isArray(obj: unknown): obj is any[]; declare function $isCallable(fn: unknown): fn is CallableFunction; -declare function $isConstructor(fn: unknown): fn is { new (...args: any[]): any }; declare function $isJSArray(obj: unknown): obj is any[]; declare function $isProxyObject(obj: unknown): obj is Proxy; -declare function $isDerivedArray(): TODO; -declare function $isGenerator(obj: unknown): obj is Generator; -declare function $isAsyncGenerator(obj: unknown): obj is AsyncGenerator; declare function $isRegExpObject(obj: unknown): obj is RegExp; declare function $isMap(obj: unknown): obj is Map; declare function $isSet(obj: unknown): obj is Set; -declare function $isShadowRealm(obj: unknown): obj is ShadowRealm; -declare function $isStringIterator(obj: unknown): obj is Iterator; -declare function $isArrayIterator(obj: unknown): obj is Iterator; declare function $isUndefinedOrNull(obj: unknown): obj is null | undefined; declare function $tailCallForwardArguments(fn: CallableFunction, thisValue: ThisType): any; /** @@ -213,94 +177,20 @@ declare function $putByIdDirectPrivate, K ext ): void; declare function $putByValDirect(obj: any, key: PropertyKey, value: any): void; -declare function $putByValWithThisSloppy(): TODO; -declare function $putByValWithThisStrict(): TODO; declare function $putInternalField( base: InternalFieldObject, number: N, value: Fields[N], ): void; -declare function $putGeneratorInternalField(): TODO; -declare function $putAsyncGeneratorInternalField(): TODO; -declare function $putArrayIteratorInternalField(): TODO; -declare function $putStringIteratorInternalField(): TODO; -declare function $superSamplerBegin(): TODO; -declare function $superSamplerEnd(): TODO; -declare function $toNumber(x: any): number; -declare function $toString(x: any): string; -declare function $toPropertyKey(x: any): PropertyKey; -/** - * Often used like - * `$toObject(this, "Class.prototype.method requires that |this| not be null or undefined");` - */ -declare function $toObject(object: any, errorMessage?: string): object; /** * ## References * - [WebKit - `emit_intrinsic_newArrayWithSize`](https://github.com/oven-sh/WebKit/blob/e1a802a2287edfe7f4046a9dd8307c8b59f5d816/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp#L2317) */ declare function $newArrayWithSize(size: number): T[]; -/** - * Optimized path for creating a new array storing objects with the same homogenous Structure - * as {@link array}. - * - * @param size the initial size of the new array - * @param array the array whose shape we want to copy - * - * @returns a new array - * - * ## References - * - [WebKit - `emit_intrinsic_newArrayWithSpecies`](https://github.com/oven-sh/WebKit/blob/e1a802a2287edfe7f4046a9dd8307c8b59f5d816/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp#L2328) - * - [WebKit - #4909](https://github.com/WebKit/WebKit/pull/4909) - * - [WebKit Bugzilla - Related Issue/Ticket](https://bugs.webkit.org/show_bug.cgi?id=245797) - */ -declare function $newArrayWithSpecies(size: number, array: T[]): T[]; -declare function $newPromise(): TODO; -declare function $createPromise(): TODO; -declare const $iterationKindKey: TODO; -declare const $iterationKindValue: TODO; -declare const $iterationKindEntries: TODO; -declare const $MAX_ARRAY_INDEX: number; -declare const $MAX_STRING_LENGTH: number; -declare const $MAX_SAFE_INTEGER: number; declare const $ModuleFetch: number; -declare const $ModuleTranslate: number; -declare const $ModuleInstantiate: number; -declare const $ModuleSatisfy: number; declare const $ModuleLink: number; -declare const $ModuleReady: number; -declare const $promiseRejectionReject: TODO; -declare const $promiseRejectionHandle: TODO; declare const $proxyFieldTarget: TODO; declare const $proxyFieldHandler: TODO; -declare const $generatorFieldState: TODO; -declare const $generatorFieldNext: TODO; -declare const $generatorFieldThis: TODO; -declare const $generatorFieldFrame: TODO; -declare const $generatorFieldContext: TODO; -declare const $GeneratorResumeModeNormal: TODO; -declare const $GeneratorResumeModeThrow: TODO; -declare const $GeneratorResumeModeReturn: TODO; -declare const $GeneratorStateCompleted: TODO; -declare const $GeneratorStateExecuting: TODO; -declare const $arrayIteratorFieldIndex: TODO; -declare const $arrayIteratorFieldIteratedObject: TODO; -declare const $arrayIteratorFieldKind: TODO; -declare const $mapIteratorFieldMapBucket: TODO; -declare const $setIteratorFieldSetBucket: TODO; -declare const $stringIteratorFieldIndex: TODO; -declare const $stringIteratorFieldIteratedString: TODO; -declare const $asyncGeneratorFieldSuspendReason: TODO; -declare const $asyncGeneratorFieldQueueFirst: TODO; -declare const $asyncGeneratorFieldQueueLast: TODO; -declare const $AsyncGeneratorStateCompleted: TODO; -declare const $AsyncGeneratorStateExecuting: TODO; -declare const $AsyncGeneratorStateAwaitingReturn: TODO; -declare const $AsyncGeneratorStateSuspendedStart: TODO; -declare const $AsyncGeneratorStateSuspendedYield: TODO; -declare const $AsyncGeneratorSuspendReasonYield: TODO; -declare const $AsyncGeneratorSuspendReasonAwait: TODO; -declare const $AsyncGeneratorSuspendReasonNone: TODO; -declare const $abstractModuleRecordFieldState: TODO; declare const $processBindingConstants: { os: typeof import("os").constants; fs: typeof import("fs").constants; @@ -424,11 +314,9 @@ declare function $overridableRequire(this: JSCommonJSModule, id: string): any; declare function $toLength(length: number): number; declare function $isTypedArrayView(obj: unknown): obj is ArrayBufferView | DataView | Uint8Array; declare function $setStateToMax(target: any, state: number): void; -declare function $trunc(target: number): number; declare function $newPromiseCapability(C: PromiseConstructor): TODO; /** @deprecated, use new TypeError instead */ declare function $makeTypeError(message: string): TypeError; -declare function $newHandledRejectedPromise(error: unknown): Promise; declare const __internal: unique symbol; interface InternalFieldObject { @@ -542,7 +430,6 @@ declare function $ERR_MISSING_ARGS(...args: [string, ...string[]]): TypeError; declare function $ERR_MISSING_ARGS(oneOf: string[]): TypeError; declare function $ERR_INVALID_RETURN_VALUE(expected_type: string, name: string, actual_value: any): TypeError; declare function $ERR_TLS_INVALID_PROTOCOL_VERSION(a: string, b: string): TypeError; -declare function $ERR_TLS_PROTOCOL_VERSION_CONFLICT(a: string, b: string): TypeError; declare function $ERR_INVALID_IP_ADDRESS(ip: any): TypeError; declare function $ERR_INVALID_ADDRESS_FAMILY(addressType, host, port): RangeError; declare function $ERR_OUT_OF_RANGE(name: string, reason: string, value): RangeError; @@ -571,8 +458,6 @@ declare function $ERR_UNESCAPED_CHARACTERS(arg): TypeError; declare function $ERR_HTTP_INVALID_STATUS_CODE(code): RangeError; declare function $ERR_UNHANDLED_ERROR(err?): Error; declare function $ERR_BUFFER_OUT_OF_BOUNDS(name?: string): RangeError; -declare function $ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(value, expected): TypeError; -declare function $ERR_CRYPTO_INCOMPATIBLE_KEY(name, value): Error; declare function $ERR_CHILD_PROCESS_IPC_REQUIRED(where): Error; declare function $ERR_CHILD_PROCESS_STDIO_MAXBUFFER(message): Error; declare function $ERR_INVALID_ASYNC_ID(name, value): RangeError; @@ -591,7 +476,6 @@ declare function $ERR_INSPECTOR_NOT_WORKER(): Error; declare function $ERR_INSPECTOR_COMMAND(message: string): Error; declare function $ERR_WORKER_UNSUPPORTED_OPERATION(message: string): TypeError; declare function $ERR_SERVER_NOT_RUNNING(): Error; -declare function $ERR_IPC_CHANNEL_CLOSED(): Error; declare function $ERR_SOCKET_BAD_TYPE(): Error; declare function $ERR_ZLIB_INITIALIZATION_FAILED(): Error; declare function $ERR_IPC_ONE_PIPE(): Error; @@ -642,9 +526,7 @@ declare function $ERR_HTTP2_OUT_OF_STREAMS(): Error; declare function $ERR_HTTP_BODY_NOT_ALLOWED(): Error; declare function $ERR_HTTP_SOCKET_ASSIGNED(): Error; declare function $ERR_DIR_CLOSED(): Error; -declare function $ERR_INVALID_MIME_SYNTAX(production: string, str: string, invalidIndex: number | -1): TypeError; declare function $ERR_SOCKET_CONNECTION_TIMEOUT(): Error; -declare function $ERR_INVALID_HANDLE_TYPE(): TypeError; declare function $ERR_TLS_HANDSHAKE_TIMEOUT(): Error; declare function $ERR_VM_MODULE_STATUS(reason: string): Error; declare function $ERR_VM_MODULE_ALREADY_LINKED(): Error; @@ -679,11 +561,6 @@ declare function $min(a: number, b: number): number; declare function $checkBufferRead(buf: Buffer, offset: number, byteLength: number): undefined; -/** - * Schedules a callback to be invoked as a microtask. - */ -declare function $enqueueJob any>(callback: T, ...args: Parameters): void; - interface Map { $get: typeof Map.prototype.get; $set: typeof Map.prototype.set; @@ -694,8 +571,6 @@ interface ObjectConstructor { $defineProperties: typeof Object.defineProperties; } -declare const $Object: ObjectConstructor; - /** gets a property on an object */ declare function $getByIdDirect(obj: T, key: K): T[K]; @@ -715,9 +590,3 @@ declare function $getByIdDirectPrivate( declare var $Promise: PromiseConstructor; declare function $isPromise(value: unknown): value is Promise; - -declare type $ReadableStream = ReadableStream; -declare type $ReadableStreamBYOBReader = ReadableStreamBYOBReader; -declare type $ReadableStreamDefaultReader = ReadableStreamDefaultReader; -declare type $ReadableStreamDefaultController = ReadableStreamDefaultController; -declare type $ReadableStreamDirectController = ReadableStreamDirectController; diff --git a/src/jsc/JSValue.rs b/src/jsc/JSValue.rs index c4a1b5d59272..d7266e32d7b4 100644 --- a/src/jsc/JSValue.rs +++ b/src/jsc/JSValue.rs @@ -181,12 +181,6 @@ impl JSValue { ) { self.then2(global, ctx, resolve, reject) } - - /// Reinterpret a raw pointer's address as a JSValue bit-pattern. - #[inline] - pub fn cast(ptr: *const T) -> JSValue { - JSValue(ptr as usize, PhantomData) - } } // `pub fn format(...) !void { @compileError(...) }` — intentionally NOT diff --git a/src/jsc/RuntimeTranspilerStore.rs b/src/jsc/RuntimeTranspilerStore.rs index 640ad4635a6c..7a3b205bb39b 100644 --- a/src/jsc/RuntimeTranspilerStore.rs +++ b/src/jsc/RuntimeTranspilerStore.rs @@ -940,7 +940,6 @@ impl TranspilerJob { input_file_fd, path.text, hash, - loader, Fd::INVALID, package_json, ); @@ -968,7 +967,6 @@ impl TranspilerJob { input_file_fd, path.text, hash, - loader, Fd::INVALID, package_json, ); diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index fd5eb561a0f8..385a97b5eece 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -3770,8 +3770,6 @@ impl VirtualMachine { if main.is_empty() { return; } - let ext = bun_paths::extension(main); - let loader = self.transpiler.options.loader(ext); let watcher = self.bun_watcher_ptr(); if !watcher.is_null() { // SAFETY: `bun_watcher` is a live `Box` leaked in @@ -3781,7 +3779,7 @@ impl VirtualMachine { // and `add_file_by_path_slow` serializes the inner watchlist write // via `Watcher.mutex`. Borrow is scoped to this single // mutex-guarded call. - let _ = unsafe { (*watcher).add_file_by_path_slow(main, loader) }; + let _ = unsafe { (*watcher).add_file_by_path_slow(main) }; } } diff --git a/src/jsc/hot_reloader.rs b/src/jsc/hot_reloader.rs index b94eb1050993..c6cba43498c5 100644 --- a/src/jsc/hot_reloader.rs +++ b/src/jsc/hot_reloader.rs @@ -32,12 +32,6 @@ pub enum ImportWatcher { Watch(Box), } -// Drift guard for the bun_watcher CYCLEBREAK `Loader` newtype: its `File` -// constant must mirror `bun_ast::Loader::File` — -// the watcher stores that value for auto-watched directories. This crate sees -// both types, so the compile-time check lives here. -const _: () = assert!(bun_watcher::Loader::File.0 == bun_ast::Loader::File as u8); - impl ImportWatcher { /// Look up the `package_json` column for `hash` under the watcher's /// mutex. @@ -62,13 +56,9 @@ impl ImportWatcher { } #[inline] - pub fn add_file_by_path_slow(&mut self, file_path: &[u8], loader: bun_ast::Loader) -> bool { - // Note: bun_watcher::Loader is an opaque newtype over u8; - // wrap the bun_ast::Loader discriminant. + pub fn add_file_by_path_slow(&mut self, file_path: &[u8]) -> bool { match self { - ImportWatcher::Hot(w) | ImportWatcher::Watch(w) => { - w.add_file_by_path_slow(file_path, bun_watcher::Loader(loader as u8)) - } + ImportWatcher::Hot(w) | ImportWatcher::Watch(w) => w.add_file_by_path_slow(file_path), ImportWatcher::None => true, } } @@ -79,22 +69,15 @@ impl ImportWatcher { fd: Fd, file_path: &[u8], hash: bun_watcher::HashType, - loader: bun_ast::Loader, dir_fd: Fd, // Note: bun_watcher::PackageJSON is an opaque forward-decl; // callers cast from `&bun_resolver::PackageJSON`. package_json: Option<&'static bun_watcher::PackageJSON>, ) -> bun_sys::Result { match self { - ImportWatcher::Hot(watcher) | ImportWatcher::Watch(watcher) => watcher - .add_file::( - fd, - file_path, - hash, - bun_watcher::Loader(loader as u8), - dir_fd, - package_json, - ), + ImportWatcher::Hot(watcher) | ImportWatcher::Watch(watcher) => { + watcher.add_file::(fd, file_path, hash, dir_fd, package_json) + } ImportWatcher::None => Ok(bun_watcher::FdOwnership::Caller), } } diff --git a/src/runtime/cli/test_command.rs b/src/runtime/cli/test_command.rs index d27f34019e08..782baf95af73 100644 --- a/src/runtime/cli/test_command.rs +++ b/src/runtime/cli/test_command.rs @@ -2717,8 +2717,7 @@ impl TestCommand { let watcher = unsafe { &mut *vm.bun_watcher.cast::() }; for path in &changed_module_graph_files { - let loader = vm.transpiler.options.loader(bun_path::extension(path)); - let _ = watcher.add_file_by_path_slow(path, loader); + let _ = watcher.add_file_by_path_slow(path); } } diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index edb355d3b465..cf14733469aa 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -2779,7 +2779,6 @@ fn transpile_source_code_inner( is_node_override, path, hash, - loader, package_json, ); } @@ -2831,7 +2830,6 @@ fn transpile_source_code_inner( is_node_override, path, hash, - loader, package_json, ); } @@ -3599,17 +3597,9 @@ fn transpile_source_code_inner( // type. let watcher = unsafe { &mut *(*jsc_vm).bun_watcher.cast::() }; - if !matches!( - watcher.add_file::( - input_fd, - path.text, - hash, - loader, - bun_sys::Fd::INVALID, - None, - ), - Ok(bun_watcher::FdOwnership::Watcher) - ) { + let added = + watcher.add_file::(input_fd, path.text, hash, bun_sys::Fd::INVALID, None); + if !matches!(added, Ok(bun_watcher::FdOwnership::Watcher)) { // Not adopted (already watched, or add failed); close the // fd this arm opened. if input_fd.is_valid() { @@ -3694,7 +3684,6 @@ fn maybe_watch_file( is_node_override: bool, path: &Fs::Path, hash: u32, - loader: Loader, package_json: Option<&'static bun_watcher::PackageJSON>, ) { // SAFETY: per fn contract — `jsc_vm` is the live per-thread VM. @@ -3718,7 +3707,6 @@ fn maybe_watch_file( input_file_fd, path.text, hash, - loader, bun_sys::Fd::INVALID, package_json, ), diff --git a/src/runtime/node/node.classes.ts b/src/runtime/node/node.classes.ts index c6455ed259da..c5d348be8cdb 100644 --- a/src/runtime/node/node.classes.ts +++ b/src/runtime/node/node.classes.ts @@ -94,10 +94,6 @@ export default [ fn: "doUnref", length: 0, }, - hasRef: { - fn: "hasRef", - length: 0, - }, close: { fn: "doClose", length: 0, @@ -325,7 +321,6 @@ export default [ symlinkSync: { async: false, fn: "symlinkSync", length: 3 }, truncate: { async: true, fn: "truncate", length: 3 }, truncateSync: { async: false, fn: "truncateSync", length: 2 }, - unwatchFile: { async: true, fn: "unwatchFile", length: 2 }, unlink: { async: true, fn: "unlink", length: 2 }, unlinkSync: { async: false, fn: "unlinkSync", length: 1 }, utimes: { async: true, fn: "utimes", length: 4 }, diff --git a/src/runtime/node/node_fs.rs b/src/runtime/node/node_fs.rs index dd6773bdc442..4c2e56f4e7e1 100644 --- a/src/runtime/node/node_fs.rs +++ b/src/runtime/node/node_fs.rs @@ -4381,7 +4381,6 @@ pub mod args { } } - pub(crate) type UnwatchFile = (); pub(crate) type Watch<'a> = super::Watcher::Arguments<'a>; // `StatWatcher::Arguments` owns its `PathLike` (no borrowed slice), so it // has no lifetime parameter — unlike `Watcher::Arguments<'a>` above. @@ -4553,7 +4552,6 @@ pub mod ret { pub(crate) type Symlink = (); pub(crate) type Truncate = (); pub(crate) type Unlink = (); - pub(crate) type UnwatchFile = (); pub(crate) type Watch = JSValue; pub(crate) type WatchFile = JSValue; pub(crate) type Utimes = (); @@ -8096,14 +8094,6 @@ impl NodeFS { } } - pub(crate) fn unwatch_file( - &mut self, - _: args::UnwatchFile, - _: Flavor, - ) -> Maybe { - Maybe::::todo() - } - pub(crate) fn utimes(&mut self, args: &args::Utimes, _: Flavor) -> Maybe { #[cfg(windows)] { diff --git a/src/runtime/node/node_fs_binding.rs b/src/runtime/node/node_fs_binding.rs index 99415ff3c126..37cad92ffa20 100644 --- a/src/runtime/node/node_fs_binding.rs +++ b/src/runtime/node/node_fs_binding.rs @@ -325,29 +325,6 @@ impl Binding { Ok(res) => Ok(res), } } - - /// `callSync(.unwatchFile)` — `Arguments == void`. - pub(crate) fn unwatch_file( - this: &Self, - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - // SAFETY: JS-thread borrow of the per-thread VM. - let vm: &VirtualMachine = global.bun_vm(); - let _slice = ArgumentsSlice::init(vm, frame.arguments()); - - if global.has_exception() { - return Ok(JSValue::ZERO); - } - - match this - .node_fs - .with_mut(|nfs| nfs.unwatch_file((), Flavor::Sync)) - { - Err(ref err) => Err(global.throw_value(err.to_js(global))), - Ok(()) => Ok(JSValue::UNDEFINED), - } - } } /// Generates the `pub const = call{Async,Sync}(.)` block. diff --git a/src/runtime/node/node_fs_watcher.rs b/src/runtime/node/node_fs_watcher.rs index b581a1c3ddf7..a9dda56c5a10 100644 --- a/src/runtime/node/node_fs_watcher.rs +++ b/src/runtime/node/node_fs_watcher.rs @@ -993,15 +993,6 @@ impl FSWatcher { Ok(JSValue::UNDEFINED) } - #[bun_jsc::host_fn(method)] - pub(crate) fn has_ref( - &self, - _global: &JSGlobalObject, - _frame: &CallFrame, - ) -> JsResult { - Ok(JSValue::from(self.persistent.get())) - } - // this can be called from Watcher Thread or JS Context Thread pub(crate) fn ref_task(&self) -> bool { let _guard = self.mutex.lock_guard(); diff --git a/src/runtime/node/quic/endpoint.rs b/src/runtime/node/quic/endpoint.rs index e8ce43d7bb33..3f2a619565c8 100644 --- a/src/runtime/node/quic/endpoint.rs +++ b/src/runtime/node/quic/endpoint.rs @@ -2569,13 +2569,6 @@ impl QuicEndpoint { self.with_state(|s| s.busy = busy as u8); Ok(JSValue::UNDEFINED) } - pub(crate) fn do_ref(&self, _g: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let want = frame.arguments_as_array::<1>()[0].to_boolean(); - let ctx = bun_io::js_vm_ctx(); - self.poll_ref - .with_mut(|p| if want { p.ref_(ctx) } else { p.unref(ctx) }); - Ok(JSValue::UNDEFINED) - } fn build_sni_contexts( global: &JSGlobalObject, entries: JSValue, diff --git a/src/runtime/node/quic/quic.classes.ts b/src/runtime/node/quic/quic.classes.ts index 21228eb09e3b..7b9bcc0e2598 100644 --- a/src/runtime/node/quic/quic.classes.ts +++ b/src/runtime/node/quic/quic.classes.ts @@ -34,10 +34,6 @@ export default [ fn: "markBusy", length: 1, }, - ref: { - fn: "doRef", - length: 1, - }, setSNIContexts: { fn: "setSniContexts", length: 2, @@ -87,10 +83,6 @@ export default [ fn: "gracefulClose", length: 1, }, - silentClose: { - fn: "silentClose", - length: 0, - }, updateKey: { fn: "updateKey", length: 0, diff --git a/src/runtime/node/quic/session.rs b/src/runtime/node/quic/session.rs index 6eb816790bd4..194d379570d8 100644 --- a/src/runtime/node/quic/session.rs +++ b/src/runtime/node/quic/session.rs @@ -1941,15 +1941,6 @@ impl QuicSession { } Ok(JSValue::UNDEFINED) } - pub(crate) fn silent_close(&self, _g: &JSGlobalObject, _f: &CallFrame) -> JsResult { - if !self.destroyed.get() { - if let Some(c) = self.conn() { - c.abort(); - self.schedule_process(); - } - } - Ok(JSValue::UNDEFINED) - } pub(crate) fn destroy(&self, global: &JSGlobalObject, frame: &CallFrame) -> JsResult { if self.destroyed.get() { return Ok(JSValue::UNDEFINED); diff --git a/src/sys/lib.rs b/src/sys/lib.rs index 4a1c4833770e..76c28c6b7102 100644 --- a/src/sys/lib.rs +++ b/src/sys/lib.rs @@ -6033,10 +6033,6 @@ impl DynLib { // Windows: FreeLibrary via windows mod; intentionally leaked here // (close is a no-op on Windows in our usage). } - #[inline] - pub fn handle(&self) -> *mut c_void { - self.handle - } } /// `RTLD_*` flags for `dlopen`. @@ -7799,7 +7795,7 @@ pub(crate) fn make_path_w(dir: Fd, sub_path: &[u16]) -> Maybe<()> { // `std.posix` — wider surface than `bun_errno::posix` (which only has // mode_t/E/S/errno). Dependents (`bun_resolver`, `bun_md`, `bun_crash`, // `bun_threading`) reach for `Sigaction`, `getrlimit`, `tcgetattr`, raw -// `read`/`write`/`poll`, `dl_iterate_phdr` etc. We re-export the errno stub +// `poll`, `dl_iterate_phdr` etc. We re-export the errno stub // and layer the libc bits on top so `bun_sys::posix::*` is the single import. // ────────────────────────────────────────────────────────────────────────── pub mod posix { @@ -8017,22 +8013,6 @@ pub mod posix { pub(crate) tv_nsec: i64, } - // ── raw I/O (no `Maybe` wrapping) ── - #[cfg(unix)] - #[inline] - pub unsafe fn read(fd: c_int, buf: *mut u8, count: usize) -> isize { - #[cfg(any(target_os = "linux", target_os = "android"))] - { - // SAFETY: caller contract — `buf` points to `count` writable bytes. - unsafe { super::linux_syscall::read_raw(fd, buf, count) } - } - #[cfg(not(any(target_os = "linux", target_os = "android")))] - { - // SAFETY: caller contract — `buf` points to `count` writable bytes. - unsafe { libc::read(fd, buf.cast(), count) } - } - } - // ── poll ── /// `struct pollfd`. #[cfg(unix)] diff --git a/src/sys/linux_syscall.rs b/src/sys/linux_syscall.rs index 362c4a766d76..ae5cc0041722 100644 --- a/src/sys/linux_syscall.rs +++ b/src/sys/linux_syscall.rs @@ -385,7 +385,7 @@ pub(crate) unsafe fn pwritev( // `-errno` returns, which callers decode via `GetErrno for isize`. // ────────────────────────────────────────────────────────────────────────── -/// Raw `read(2)` — libc-convention return (for `linux::read` / `posix::read`). +/// Raw `read(2)` — libc-convention return (for `linux::read`). /// /// This is a libc-convention thunk: callers may pass `fd == -1` (expecting /// EBADF), `buf == NULL` with `count == 0` (expecting `0`), or an diff --git a/src/tcc_sys/lib.rs b/src/tcc_sys/lib.rs index fcc047c0fa54..58593e1e124e 100644 --- a/src/tcc_sys/lib.rs +++ b/src/tcc_sys/lib.rs @@ -2,6 +2,5 @@ #![warn(unused_must_use)] pub mod tcc; pub use tcc::{ - Config, ConfigErr, Error, ErrorFunc, OutputFormat, State, Symbol, SymbolCallback, TCCErrorFunc, - TCCState, + Config, ConfigErr, Error, ErrorFunc, OutputFormat, State, Symbol, TCCErrorFunc, TCCState, }; diff --git a/src/tcc_sys/tcc.rs b/src/tcc_sys/tcc.rs index 9e2d2841f141..cb1ee253b85b 100644 --- a/src/tcc_sys/tcc.rs +++ b/src/tcc_sys/tcc.rs @@ -122,9 +122,6 @@ pub enum OutputFormat { // allowing for interop with other APIs taking `*mut c_void` pointers. bun_opaque::opaque_ffi! { pub struct Symbol; } -pub type SymbolCallback = - unsafe extern "C" fn(ctx: *mut c_void, name: *const c_char, val: *const Symbol); - bun_opaque::opaque_ffi! { /// Opaque TinyCC compilation state. Always handled via `*mut State` / `&mut State`. pub struct State; diff --git a/src/watcher/Watcher.rs b/src/watcher/Watcher.rs index a66df8e900ea..08a14de2b523 100644 --- a/src/watcher/Watcher.rs +++ b/src/watcher/Watcher.rs @@ -8,7 +8,6 @@ use bun_core::{ThreadLock, ZStr, feature_flags, output as Output, strings, zstr} use bun_sys::{self as sys, Fd}; use bun_threading::Mutex; -use crate::Loader; use crate::watcher_trace as WatcherTrace; // Android: same kernel inotify ABI as glibc/musl Linux, so list both. @@ -517,7 +516,6 @@ impl Watcher { fd: Fd, file_path: &[u8], hash: HashType, - loader: Loader, parent_hash: HashType, package_json: Option<&'static PackageJSON>, ) -> sys::Result { @@ -576,7 +574,6 @@ impl Watcher { fd, hash, count: 0, - loader, parent_hash, package_json, kind: WatchItemKind::File, @@ -661,7 +658,6 @@ impl Watcher { fd, hash, count: 0, - loader: Loader::File, parent_hash, kind: WatchItemKind::Directory, package_json: None, @@ -678,7 +674,6 @@ impl Watcher { fd: Fd, file_path: &[u8], hash: HashType, - loader: Loader, dir_fd: Fd, package_json: Option<&'static PackageJSON>, ) -> sys::Result { @@ -742,7 +737,6 @@ impl Watcher { fd, file_path, hash, - loader, parent_dir_hash, package_json, ) { @@ -814,7 +808,7 @@ impl Watcher { /// Returns: /// - true if the file is successfully added to the watchlist or already watched /// - false if the file cannot be opened or added to the watchlist - pub fn add_file_by_path_slow(&mut self, file_path: &[u8], loader: Loader) -> bool { + pub fn add_file_by_path_slow(&mut self, file_path: &[u8]) -> bool { if file_path.is_empty() { return false; } @@ -851,7 +845,7 @@ impl Watcher { #[cfg(not(any(target_os = "macos", target_os = "freebsd")))] let fd: Fd = Fd::INVALID; - let res = self.add_file::(fd, file_path, hash, loader, Fd::INVALID, None); + let res = self.add_file::(fd, file_path, hash, Fd::INVALID, None); match res { Ok(ownership) => { // Not adopted (another thread won the add race); close the @@ -875,7 +869,6 @@ impl Watcher { fd: Fd, file_path: &[u8], hash: HashType, - loader: Loader, dir_fd: Fd, package_json: Option<&'static PackageJSON>, ) -> sys::Result { @@ -904,7 +897,6 @@ impl Watcher { fd, file_path, hash, - loader, dir_fd, package_json, ); @@ -1055,7 +1047,6 @@ pub struct WatchItem { pub file_path: Cow<'static, [u8]>, // filepath hash for quick comparison pub hash: u32, - pub loader: Loader, pub fd: Fd, pub count: u32, pub parent_hash: u32, diff --git a/src/watcher/lib.rs b/src/watcher/lib.rs index 935ba8504c05..76619cf6836e 100644 --- a/src/watcher/lib.rs +++ b/src/watcher/lib.rs @@ -39,22 +39,3 @@ pub use watcher_impl::{ MAX_EVICTION_COUNT, Op, PackageJSON, REQUIRES_FILE_DESCRIPTORS, WATCH_OPEN_FLAGS, WatchEvent, WatchItem, WatchItemColumns, WatchItemIndex, WatchItemKind, WatchList, Watcher, WatcherContext, }; - -// ─── upward-crate placeholders (CYCLEBREAK) ─────────────────────────────── -// -// These belong to higher-tier crates that don't yet expose a usable surface -// to depend on. Watcher only stores/passes them through; never dereferenced. - -/// Opaque forward-decl of `bun_ast::Loader` (cycle-break: bun_watcher sits -/// below bun_ast in the crate graph). Watcher only stores the value in -/// `WatchItem.loader` and passes it through; callers construct it via -/// `Loader(bun_ast::Loader as u8)` at the boundary. -#[derive(Clone, Copy, Default)] -pub struct Loader(pub u8); -impl Loader { - /// Mirrors `bun_ast::Loader::File as u8`; - /// keep the discriminant in sync with `src/ast/loader.rs`. A compile-time - /// drift guard lives in `src/jsc/hot_reloader.rs` (a crate that sees both - /// types). - pub const File: Loader = Loader(5); -} From b5eac2eded4020f16e53374e6a32b0f5623d7efa Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:30:54 +0000 Subject: [PATCH 3/6] Gate the MaybeTodo import off macOS/FreeBSD, add the dead-symbols source lint --- .github/workflows/source-lints.yml | 2 + src/runtime/node/node_fs.rs | 5 +- ...ols-h2-inbound-watcher-loader-misc.test.ts | 236 ++++++++++++++++++ 3 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 test/internal/source-lints/dead-symbols-h2-inbound-watcher-loader-misc.test.ts diff --git a/.github/workflows/source-lints.yml b/.github/workflows/source-lints.yml index ffbca8fcfc30..1e3945d5345d 100644 --- a/.github/workflows/source-lints.yml +++ b/.github/workflows/source-lints.yml @@ -15,6 +15,7 @@ on: - "src/**/*.rs" - "src/**/*.classes.ts" - "src/codegen/class-definitions.ts" + - "src/js/builtins.d.ts" - "src/jsc/bindings/**" - "packages/bun-types/redis.d.ts" - "scripts/build/**" @@ -33,6 +34,7 @@ on: - "src/**/*.rs" - "src/**/*.classes.ts" - "src/codegen/class-definitions.ts" + - "src/js/builtins.d.ts" - "src/jsc/bindings/**" - "packages/bun-types/redis.d.ts" - "scripts/build/**" diff --git a/src/runtime/node/node_fs.rs b/src/runtime/node/node_fs.rs index 4c2e56f4e7e1..b7da64edf7ac 100644 --- a/src/runtime/node/node_fs.rs +++ b/src/runtime/node/node_fs.rs @@ -183,8 +183,11 @@ mod node { // `validators::*` — `super::util::validators` is a `pub use` of a // crate-private module, which trips E0365 if we `pub use` it again. Import it // privately at file scope instead and call as `validators::foo` directly. -use super::MaybeTodo as _; use super::util::validators; +// `Maybe::todo()` is only called from the `lchmod` / `copy_file` arms that are +// still stubs, and macOS and FreeBSD have none of those. +#[cfg(not(any(target_os = "macos", target_os = "freebsd")))] +use super::MaybeTodo as _; // Trait imports for inherent-looking method calls on upstream types: // - `bun_sys::FdExt` → `Fd::close()` diff --git a/test/internal/source-lints/dead-symbols-h2-inbound-watcher-loader-misc.test.ts b/test/internal/source-lints/dead-symbols-h2-inbound-watcher-loader-misc.test.ts new file mode 100644 index 000000000000..de8d469370f0 --- /dev/null +++ b/test/internal/source-lints/dead-symbols-h2-inbound-watcher-loader-misc.test.ts @@ -0,0 +1,236 @@ +// Guards against reintroduction of code removed as dead: +// +// - the pre-engine inbound frame path of h2_frame_parser.rs (everything rustc +// reported once the file-level `allow(dead_code)` was dropped) and the +// HPACK static-table JSString cache (`Bun::Http2CommonStrings`) whose only +// reader was that path's header decoder; +// - native class methods no built-in JS calls (QuicSession.silentClose, +// QuicEndpoint.ref, NodeJSFS.unwatchFile, FSWatcher.hasRef), removed from +// the .classes.ts declarations together with their Rust implementations; +// - the watcher's write-only `WatchItem.loader` column, the `bun_watcher::Loader` +// cycle-break newtype that existed only to carry it, and the `loader` +// parameters that threaded it through every `add_file` caller; +// - `pub` helpers in bun_core, bun_sys, bun_jsc and tcc_sys with no caller in +// any crate, and `$`-declarations in src/js/builtins.d.ts no built-in uses. +// +// Every Rust removal was validated by `cargo check --workspace` on every CI +// target triple and a full `bun bd` build (which regenerates the class codegen +// that consumed the .classes.ts entries); the builtins.d.ts removals by +// `tsc -p src/js` reporting the same diagnostics before and after. +// +// This is a source-tree lint: it reads files from src/ and does not touch the +// built binary, so it belongs in test/internal/source-lints/ per the README. +// Content checks read the working tree; the deleted-file check reads the +// committed tree (HEAD), because `git stash` round-trips can temporarily +// restore files a branch deletes (see dead-code-escapes.test.ts). + +import { expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import path from "node:path"; + +const repoRoot = path.resolve(import.meta.dir, "..", "..", ".."); + +function src(p: string): string { + return readFileSync(path.join(repoRoot, p), "utf8"); +} + +function headTree(): Set { + const r = Bun.spawnSync({ + cmd: ["git", "-C", repoRoot, "ls-tree", "-r", "--name-only", "-z", "HEAD"], + stdout: "pipe", + stderr: "pipe", + }); + if (r.exitCode !== 0) { + throw new Error(`git ls-tree HEAD failed: ${r.stderr.toString()}`); + } + return new Set(r.stdout.toString().split("\0").filter(Boolean)); +} + +function resurrected(checks: Array<[string, RegExp]>): string[] { + const byFile = new Map(); + return checks + .filter(([file, re]) => { + let text = byFile.get(file); + if (text === undefined) byFile.set(file, (text = src(file))); + return re.test(text); + }) + .map(([file, re]) => `${file}: ${re.source}`); +} + +/** The text from `opener` up to the next line that is exactly `}` (the end of that top-level block). */ +function block(text: string, opener: string): string { + const start = text.indexOf(opener); + expect(start).toBeGreaterThan(-1); + const end = text.indexOf("\n}\n", start); + expect(end).toBeGreaterThan(start); + return text.slice(start, end); +} + +test("h2_frame_parser.rs stays under dead_code = deny", () => { + const text = src("src/runtime/api/bun/h2_frame_parser.rs"); + // The inner attribute at the top of the file must not suppress dead_code + // again; rustc is what keeps the retired inbound path from growing back. + const innerAttrs = text.slice(0, text.indexOf("\nuse ")); + expect(innerAttrs).not.toMatch(/\bdead_code\b/); +}); + +test("the legacy h2 inbound path does not reappear", () => { + const h2 = "src/runtime/api/bun/h2_frame_parser.rs"; + const checks: Array<[string, RegExp]> = [ + // The byte reader and frame dispatcher that fed the handlers. + [h2, /\bfn (read_bytes|dispatch_frame|lookup_inbound_stream)\b/], + // The per-frame handlers and their payload helper. + [h2, /\bfn handle_(incomming_payload|window_update_frame|unknown_frame|push_promise_frame|data_frame|go_away_frame|origin_frame|altsvc_frame|rst_stream_frame|ping_frame|priority_frame|continuation_frame|headers_frame|settings_frame)\b/], + [h2, /\bfn (decode_header_block|finish_headers_end_stream|adjust_window_size|increment_window_size_if_needed|send_settings_ack|dispatch_with_3_extra)\b/], + [h2, /\bstruct Payload\b/], + // Decode-direction wire helpers only those handlers used. + [h2, /\bfn (u32_from_bytes|from_bytes|update_with)\b|\benum SettingsFlags\b|\btype HeaderValue\b/], + // FullSettingsPayload lost its wire layout with `write()`: the per-entry + // type ids, the packed repr, the bytemuck impls and SettingsType went too. + [h2, /\bstruct (SettingsPayloadUnit|SettingsType)\b|_header_table_size_type|bytemuck::Pod for FullSettingsPayload/], + // Parser / stream state only the inbound path maintained. + [h2, /\b(current_frame|remaining_length|expecting_continuation|preface_received_len|read_buffer)\b/], + [h2, /\b(is_waiting_more_headers|header_block_size|header_block_count|pending_header_block|pending_header_flags)\b/], + // The HPACK static-table JSString cache the decoder read through. + [h2, /getHTTP2CommonString|get_http2_common_string/], + ["src/jsc/bindings/ZigGlobalObject.h", /Http2CommonStrings/], + ["src/jsc/bindings/ZigGlobalObject.cpp", /http2CommonStrings|getHTTP2CommonString/], + ]; + expect(resurrected(checks)).toEqual([]); +}); + +test("orphaned files stay deleted", () => { + const gone = ["src/jsc/bindings/BunHttp2CommonStrings.h", "src/jsc/bindings/BunHttp2CommonStrings.cpp"]; + const tree = headTree(); + expect(gone.filter(p => tree.has(p))).toEqual([]); +}); + +test("native methods no built-in JS calls do not reappear", () => { + const checks: Array<[string, RegExp]> = [ + ["src/runtime/node/quic/quic.classes.ts", /\bsilentClose\b|fn: "doRef"/], + ["src/runtime/node/quic/session.rs", /\bfn silent_close\b/], + ["src/runtime/node/quic/endpoint.rs", /\bfn do_ref\b/], + ["src/runtime/node/node.classes.ts", /\bunwatchFile\b/], + ["src/runtime/node/node_fs_binding.rs", /\bunwatch_file\b/], + ["src/runtime/node/node_fs.rs", /\bunwatch_file\b|\bUnwatchFile\b/], + ["src/runtime/node/node_fs_watcher.rs", /\bfn has_ref\b/], + ]; + expect(resurrected(checks)).toEqual([]); + + // Timeout and Immediate legitimately expose hasRef; only the FSWatcher + // declaration lost it, so scope the check to that class. + const classes = src("src/runtime/node/node.classes.ts"); + const start = classes.indexOf('name: "FSWatcher"'); + expect(start).toBeGreaterThan(-1); + const end = classes.indexOf("name: ", start + 1); + expect(end).toBeGreaterThan(start); + expect(classes.slice(start, end)).not.toMatch(/\bhasRef\b/); +}); + +test("the watcher loader column does not reappear", () => { + const checks: Array<[string, RegExp]> = [ + ["src/watcher/lib.rs", /\bstruct Loader\b|CYCLEBREAK/], + ["src/watcher/Watcher.rs", /\bloader\b/], + ["src/jsc/hot_reloader.rs", /bun_watcher::Loader\b|\bloader: bun_ast::Loader\b/], + ["src/jsc/VirtualMachine.rs", /add_file_by_path_slow\(main, loader\)/], + ["src/runtime/cli/test_command.rs", /add_file_by_path_slow\(path, loader\)/], + ["src/bundler/bundle_v2.rs", /bun_watcher::Loader\(/], + ]; + expect(resurrected(checks)).toEqual([]); +}); + +test("dead bun_core / bun_sys / bun_jsc / tcc_sys helpers do not reappear", () => { + const checks: Array<[string, RegExp]> = [ + // bun_core::util::{Mutex, RwLock}::get_mut, Timespec::new. + ["src/bun_core/util.rs", /\bpub fn get_mut\(&mut self\) -> &mut T\b/], + ["src/bun_core/util.rs", /\bfn new\(sec: i64, nsec: i64\)/], + // RawSlice::from_raw. + ["src/bun_core/lib.rs", /\bunsafe fn from_raw\(p: \*const \[T\]\)/], + // DynLib::handle. + ["src/sys/lib.rs", /\bfn handle\(&self\) -> \*mut c_void\b/], + // JSValue::cast. + ["src/jsc/JSValue.rs", /\bfn cast\(ptr: \*const T\)/], + // tcc_sys::SymbolCallback. + ["src/tcc_sys/tcc.rs", /\bSymbolCallback\b/], + ["src/tcc_sys/lib.rs", /\bSymbolCallback\b/], + ]; + expect(resurrected(checks)).toEqual([]); + + // Unaligned::new (other types in util.rs keep a `new(value: T)`), and + // posix::read (bun_sys::linux::read has the same signature and is live). + expect(block(src("src/bun_core/util.rs"), "impl Unaligned {")).not.toMatch(/\bfn new\b/); + expect(block(src("src/sys/lib.rs"), "pub mod posix {")).not.toMatch(/\bfn read\b/); +}); + +test("unreferenced builtins.d.ts declarations do not reappear", () => { + const dts = src("src/js/builtins.d.ts"); + const names = [ + "getByValWithThis", + "getPrototypeOf", + "fulfillPromise", + "getGeneratorInternalField", + "getAsyncGeneratorInternalField", + "getAbstractModuleRecordInternalField", + "getArrayIteratorInternalField", + "getStringIteratorInternalField", + "idWithProfile", + "isConstructor", + "isDerivedArray", + "isGenerator", + "isAsyncGenerator", + "isShadowRealm", + "isStringIterator", + "isArrayIterator", + "putByValWithThisSloppy", + "putByValWithThisStrict", + "putGeneratorInternalField", + "putAsyncGeneratorInternalField", + "putArrayIteratorInternalField", + "putStringIteratorInternalField", + "superSamplerBegin", + "superSamplerEnd", + "toNumber", + "toString", + "toPropertyKey", + "toObject", + "newArrayWithSpecies", + "newPromise", + "createPromise", + "iterationKindKey", + "MAX_SAFE_INTEGER", + "ModuleTranslate", + "ModuleReady", + "promiseRejectionReject", + "generatorFieldState", + "GeneratorResumeModeNormal", + "GeneratorStateCompleted", + "arrayIteratorFieldIndex", + "mapIteratorFieldMapBucket", + "setIteratorFieldSetBucket", + "stringIteratorFieldIndex", + "asyncGeneratorFieldSuspendReason", + "AsyncGeneratorStateCompleted", + "AsyncGeneratorSuspendReasonYield", + "abstractModuleRecordFieldState", + "trunc", + "newHandledRejectedPromise", + "ERR_TLS_PROTOCOL_VERSION_CONFLICT", + "ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE", + "ERR_CRYPTO_INCOMPATIBLE_KEY", + "ERR_IPC_CHANNEL_CLOSED", + "ERR_INVALID_MIME_SYNTAX", + "enqueueJob", + "Object", + "ReadableStream", + "ReadableStreamBYOBReader", + "ReadableStreamDefaultReader", + "ReadableStreamDefaultController", + "ReadableStreamDirectController", + ]; + const declared = names.filter(name => + new RegExp(`^declare (?:function|const|var|type) \\$${name}\\b`, "m").test(dts), + ); + expect(declared).toEqual([]); + // $ERR_INVALID_HANDLE_TYPE stays, but only once. + expect(dts.match(/^declare function \$ERR_INVALID_HANDLE_TYPE\b/gm)?.length).toBe(1); +}); From 1e788b0362fb53fef5743d488d44e9f149e04cf5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:42:33 +0000 Subject: [PATCH 4/6] Format the dead-symbols lint --- ...symbols-h2-inbound-watcher-loader-misc.test.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/internal/source-lints/dead-symbols-h2-inbound-watcher-loader-misc.test.ts b/test/internal/source-lints/dead-symbols-h2-inbound-watcher-loader-misc.test.ts index de8d469370f0..9811eac83896 100644 --- a/test/internal/source-lints/dead-symbols-h2-inbound-watcher-loader-misc.test.ts +++ b/test/internal/source-lints/dead-symbols-h2-inbound-watcher-loader-misc.test.ts @@ -80,8 +80,14 @@ test("the legacy h2 inbound path does not reappear", () => { // The byte reader and frame dispatcher that fed the handlers. [h2, /\bfn (read_bytes|dispatch_frame|lookup_inbound_stream)\b/], // The per-frame handlers and their payload helper. - [h2, /\bfn handle_(incomming_payload|window_update_frame|unknown_frame|push_promise_frame|data_frame|go_away_frame|origin_frame|altsvc_frame|rst_stream_frame|ping_frame|priority_frame|continuation_frame|headers_frame|settings_frame)\b/], - [h2, /\bfn (decode_header_block|finish_headers_end_stream|adjust_window_size|increment_window_size_if_needed|send_settings_ack|dispatch_with_3_extra)\b/], + [ + h2, + /\bfn handle_(incomming_payload|window_update_frame|unknown_frame|push_promise_frame|data_frame|go_away_frame|origin_frame|altsvc_frame|rst_stream_frame|ping_frame|priority_frame|continuation_frame|headers_frame|settings_frame)\b/, + ], + [ + h2, + /\bfn (decode_header_block|finish_headers_end_stream|adjust_window_size|increment_window_size_if_needed|send_settings_ack|dispatch_with_3_extra)\b/, + ], [h2, /\bstruct Payload\b/], // Decode-direction wire helpers only those handlers used. [h2, /\bfn (u32_from_bytes|from_bytes|update_with)\b|\benum SettingsFlags\b|\btype HeaderValue\b/], @@ -90,7 +96,10 @@ test("the legacy h2 inbound path does not reappear", () => { [h2, /\bstruct (SettingsPayloadUnit|SettingsType)\b|_header_table_size_type|bytemuck::Pod for FullSettingsPayload/], // Parser / stream state only the inbound path maintained. [h2, /\b(current_frame|remaining_length|expecting_continuation|preface_received_len|read_buffer)\b/], - [h2, /\b(is_waiting_more_headers|header_block_size|header_block_count|pending_header_block|pending_header_flags)\b/], + [ + h2, + /\b(is_waiting_more_headers|header_block_size|header_block_count|pending_header_block|pending_header_flags)\b/, + ], // The HPACK static-table JSString cache the decoder read through. [h2, /getHTTP2CommonString|get_http2_common_string/], ["src/jsc/bindings/ZigGlobalObject.h", /Http2CommonStrings/], From 73bdc1ac3f78449ae1dec7ae4f83551f5afc1e85 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:50:51 +0000 Subject: [PATCH 5/6] Spell the remaining fs stubs as Err(sys::Error::todo()) and drop the MaybeTodo trait --- src/runtime/node.rs | 14 -------------- src/runtime/node/node_fs.rs | 12 ++++-------- ...-symbols-h2-inbound-watcher-loader-misc.test.ts | 8 ++++++-- 3 files changed, 10 insertions(+), 24 deletions(-) diff --git a/src/runtime/node.rs b/src/runtime/node.rs index e1834746a6f3..bef1abc6af1a 100644 --- a/src/runtime/node.rs +++ b/src/runtime/node.rs @@ -189,17 +189,3 @@ impl MaybeExt for Maybe { self.as_ref().err() } } - -/// Extension surface providing `Maybe::todo()` on `bun_sys::Maybe` -/// (= `core::result::Result`), the type-alias form of -/// `Maybe` used throughout `node/`. -pub trait MaybeTodo: Sized { - fn todo() -> Self; -} - -impl MaybeTodo for core::result::Result { - #[inline] - fn todo() -> Self { - Err(bun_sys::Error::todo()) - } -} diff --git a/src/runtime/node/node_fs.rs b/src/runtime/node/node_fs.rs index b7da64edf7ac..fbcc2291dd84 100644 --- a/src/runtime/node/node_fs.rs +++ b/src/runtime/node/node_fs.rs @@ -184,10 +184,6 @@ mod node { // crate-private module, which trips E0365 if we `pub use` it again. Import it // privately at file scope instead and call as `validators::foo` directly. use super::util::validators; -// `Maybe::todo()` is only called from the `lchmod` / `copy_file` arms that are -// still stubs, and macOS and FreeBSD have none of those. -#[cfg(not(any(target_os = "macos", target_os = "freebsd")))] -use super::MaybeTodo as _; // Trait imports for inherent-looking method calls on upstream types: // - `bun_sys::FdExt` → `Fd::close()` @@ -5479,7 +5475,7 @@ impl NodeFS { #[cfg(windows)] { let _ = args; - return Maybe::::todo(); + return Err(sys::Error::todo()); } #[cfg(target_os = "android")] { @@ -8678,7 +8674,7 @@ impl NodeFS { let _ = reuse_stat; // https://manpages.debian.org/testing/manpages-dev/ioctl_ficlone.2.en.html if mode.is_force_clone() { - return Maybe::::todo(); + return Err(sys::Error::todo()); } let src_fd = match Syscall::open(src, sys::O::RDONLY | sys::O::NOFOLLOW, 0o644) { @@ -8974,7 +8970,7 @@ impl NodeFS { // fall back to a non-CoW `CopyFileW`, per // Node.js' documented FICLONE_FORCE contract and matching the // Linux/FreeBSD arms above. Return a concrete ENOSYS rather - // than `Maybe::todo()` so debug builds do not panic. + // than `sys::Error::todo()` so debug builds do not panic. return Err(sys::Error { errno: SystemErrno::ENOSYS as _, syscall: sys::Tag::copyfile, @@ -9121,7 +9117,7 @@ impl NodeFS { )))] { let _ = (src, dest, mode, reuse_stat); - Maybe::::todo() + Err(sys::Error::todo()) } } diff --git a/test/internal/source-lints/dead-symbols-h2-inbound-watcher-loader-misc.test.ts b/test/internal/source-lints/dead-symbols-h2-inbound-watcher-loader-misc.test.ts index 9811eac83896..bd3248dfe695 100644 --- a/test/internal/source-lints/dead-symbols-h2-inbound-watcher-loader-misc.test.ts +++ b/test/internal/source-lints/dead-symbols-h2-inbound-watcher-loader-misc.test.ts @@ -6,7 +6,8 @@ // reader was that path's header decoder; // - native class methods no built-in JS calls (QuicSession.silentClose, // QuicEndpoint.ref, NodeJSFS.unwatchFile, FSWatcher.hasRef), removed from -// the .classes.ts declarations together with their Rust implementations; +// the .classes.ts declarations together with their Rust implementations +// (and the MaybeTodo trait whose only unconditional user was unwatchFile); // - the watcher's write-only `WatchItem.loader` column, the `bun_watcher::Loader` // cycle-break newtype that existed only to carry it, and the `loader` // parameters that threaded it through every `add_file` caller; @@ -121,8 +122,11 @@ test("native methods no built-in JS calls do not reappear", () => { ["src/runtime/node/quic/endpoint.rs", /\bfn do_ref\b/], ["src/runtime/node/node.classes.ts", /\bunwatchFile\b/], ["src/runtime/node/node_fs_binding.rs", /\bunwatch_file\b/], - ["src/runtime/node/node_fs.rs", /\bunwatch_file\b|\bUnwatchFile\b/], + ["src/runtime/node/node_fs.rs", /\bunwatch_file\b|\bUnwatchFile\b|\bMaybeTodo\b/], ["src/runtime/node/node_fs_watcher.rs", /\bfn has_ref\b/], + // unwatchFile was the only stub that needed the trait on every target; the + // remaining stubs spell `Err(sys::Error::todo())` directly. + ["src/runtime/node.rs", /\bMaybeTodo\b/], ]; expect(resurrected(checks)).toEqual([]); From 1fdfee0c297146cabb909910c7dcfa44f479593d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:03:43 +0000 Subject: [PATCH 6/6] ci: retrigger