diff --git a/src/exe_format/pe.rs b/src/exe_format/pe.rs index 18a7b6fd4365..ab7739634481 100644 --- a/src/exe_format/pe.rs +++ b/src/exe_format/pe.rs @@ -36,8 +36,6 @@ pub enum Error { SecurityDirInsideImage, #[error("UnexpectedOverlayPresent")] UnexpectedOverlayPresent, - #[error("InsufficientSpace")] - InsufficientSpace, } /// Windows PE Binary manipulation for codesigning standalone executables diff --git a/src/react_compiler/DESIGN.md b/src/react_compiler/DESIGN.md index de913aad2021..4f56e509b333 100644 --- a/src/react_compiler/DESIGN.md +++ b/src/react_compiler/DESIGN.md @@ -116,9 +116,9 @@ Node allocation uses the thread-local store (`Expr::init`, `Stmt::alloc`) so nodes land in the parser's arena. `Binding` and slice/string copies need an explicit `&Arena` (the `Codegen` context carries one). -New symbols (`$`, `t0`, `c`, `_c`) are minted via the `SymbolHost` trait +New symbols (`$`, `t0`, `c`, `_c`) are minted via the `Host` trait implemented by the parser's `P`; the import of `react/compiler-runtime` is -registered via `SymbolHost::add_import_record`. +registered via `Host::add_import_record`. ### Bail-out semantics diff --git a/src/react_compiler/hir/environment_config.rs b/src/react_compiler/hir/environment_config.rs index ea345bc15758..e6d1d020765a 100644 --- a/src/react_compiler/hir/environment_config.rs +++ b/src/react_compiler/hir/environment_config.rs @@ -92,7 +92,6 @@ pub struct EnvironmentConfig { pub enable_use_keyed_state: bool, pub validate_no_set_state_in_effects: bool, pub validate_no_derived_computations_in_effects: bool, - pub validate_no_derived_computations_in_effects_exp: bool, pub validate_no_jsx_in_try_statements: bool, pub validate_static_components: bool, pub validate_no_capitalized_calls: Option>, @@ -142,7 +141,6 @@ impl Default for EnvironmentConfig { enable_use_keyed_state: false, validate_no_set_state_in_effects: false, validate_no_derived_computations_in_effects: false, - validate_no_derived_computations_in_effects_exp: false, validate_no_jsx_in_try_statements: false, validate_static_components: false, validate_no_capitalized_calls: None, diff --git a/src/react_compiler/hir/mod.rs b/src/react_compiler/hir/mod.rs index 165326f5caf1..2c6522d6d2b6 100644 --- a/src/react_compiler/hir/mod.rs +++ b/src/react_compiler/hir/mod.rs @@ -68,10 +68,8 @@ pub use reactive::*; /// buffer's `deallocate` is a no-op. Nonetheless, HIR types must NOT own /// global-heap allocations (`String`, `Box`, `Vec`): the arena bulk- /// frees on reset without walking elements, so any nested global allocation -/// leaks per parse. Use [`StoreStr`] / [`HirBox`] / [`HirVec`] instead. +/// leaks per parse. Use [`StoreStr`] / [`HirVec`] instead. pub type HirVec = bun_alloc::AstVec; -/// Arena-backed `Box`. See [`HirVec`] for the leak rationale. -pub type HirBox = bun_alloc::AstBox; pub use bun_alloc::AstAlloc; /// Arena-owned (or `'static`) byte string. Copy; no Drop. See [`HirVec`]. pub use bun_ast::StoreStr; @@ -1478,7 +1476,7 @@ impl NonLocalBinding { // ============================================================================= /// The recursive `Box` fields here intentionally use the global -/// allocator, NOT [`HirBox`]: `Type` values are constructed and held by the +/// allocator, NOT the AST arena: `Type` values are constructed and held by the /// process-lifetime [`ShapeRegistry`](crate::hir::object_shape::ShapeRegistry), /// which outlives the per-file AST arena, so an arena-backed box would dangle /// after `Store::reset()`. The leak hazard described on [`HirVec`] does not @@ -1720,11 +1718,6 @@ pub fn is_ref_or_ref_value(ty: &Type) -> bool { is_use_ref_type(ty) || is_ref_value_type(ty) } -/// Returns true if the type is a useState result (BuiltInUseState). -pub fn is_use_state_type(ty: &Type) -> bool { - matches!(ty, Type::Object { shape_id: Some(id) } if *id == object_shape::BUILT_IN_USE_STATE_ID) -} - /// Returns true if the type is a setState function (BuiltInSetState). pub fn is_set_state_type(ty: &Type) -> bool { matches!(ty, Type::Function { shape_id: Some(id), .. } if *id == object_shape::BUILT_IN_SET_STATE_ID) diff --git a/src/react_compiler/lib.rs b/src/react_compiler/lib.rs index 3c92fef62491..1b2dde1c306b 100644 --- a/src/react_compiler/lib.rs +++ b/src/react_compiler/lib.rs @@ -33,6 +33,6 @@ pub mod program; pub use compile_result::{CompileDiagnostic, CompileOutput}; pub use options::ReactCompilerOptions; pub use program::{ - CompileResult, Host, JsxImportKind, PendingCompile, ReactCompilerState, SymbolHost, + CompileResult, Host, JsxImportKind, PendingCompile, ReactCompilerState, collect_import_bindings, finish, has_module_scope_opt_out, maybe_compile_pending, }; diff --git a/src/react_compiler/program.rs b/src/react_compiler/program.rs index 3182cf9bcee8..252ba04ad711 100644 --- a/src/react_compiler/program.rs +++ b/src/react_compiler/program.rs @@ -109,9 +109,6 @@ pub trait Host { fn add_import_record(&mut self, path: &[u8], kind: ImportKind) -> (u32, Ref); } -// Back-compat alias for the parser hook written against the previous API. -pub use Host as SymbolHost; - // ----------------------------------------------------------------------- // Constants // ----------------------------------------------------------------------- @@ -501,10 +498,6 @@ pub(crate) fn parse_fixture_pragmas(source: &[u8], opts: &mut ReactCompilerOptio b"validateNoDerivedComputationsInEffects" => { env_bool!(validate_no_derived_computations_in_effects, val) } - b"validateNoDerivedComputationsInEffectsExp" - | b"validateNoDerivedComputationsInEffects_exp" => { - env_bool!(validate_no_derived_computations_in_effects_exp, val) - } b"validateNoJsxInTryStatements" | b"validateNoJSXInTryStatements" => { env_bool!(validate_no_jsx_in_try_statements, val) } diff --git a/src/react_compiler/validation/validate_no_derived_computations_in_effects.rs b/src/react_compiler/validation/validate_no_derived_computations_in_effects.rs index 55d5071900a9..be67c3ce5fe9 100644 --- a/src/react_compiler/validation/validate_no_derived_computations_in_effects.rs +++ b/src/react_compiler/validation/validate_no_derived_computations_in_effects.rs @@ -8,1158 +8,17 @@ //! //! See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state //! -//! Port of ValidateNoDerivedComputationsInEffects_exp.ts. +//! Port of ValidateNoDerivedComputationsInEffects.ts. use std::collections::{HashMap, HashSet}; -use crate::diagnostics::{ - CompilerDiagnostic, CompilerDiagnosticDetail, CompilerError, CompilerErrorDetail, ErrorCategory, -}; +use crate::diagnostics::{CompilerError, CompilerErrorDetail, ErrorCategory}; use crate::hir::environment::Environment; -use crate::hir::visitors::{ - each_instruction_lvalue_ids, each_instruction_operand as canonical_each_instruction_operand, -}; use crate::hir::{ - ArrayElement, BlockId, Effect, EvaluationOrder, FunctionId, HirFunction, Identifier, - IdentifierId, IdentifierName, InstructionValue, ParamPattern, PlaceOrSpread, ReactFunctionType, - ReturnVariant, SourceLocation, Type, is_set_state_type, is_use_effect_hook_type, - is_use_ref_type, is_use_state_type, + ArrayElement, BlockId, FunctionId, HirFunction, Identifier, IdentifierId, InstructionValue, + PlaceOrSpread, SourceLocation, Type, is_set_state_type, is_use_effect_hook_type, }; -/// Get the user-visible name for an identifier, matching Babel's -/// loc.identifierName behavior. First checks the identifier's own name, -/// then falls back to extracting the name from the source code at the -/// given source location. This handles SSA identifiers whose names were -/// lost during compiler passes. -fn get_identifier_name_with_loc( - id: IdentifierId, - identifiers: &[Identifier], - loc: &Option, - source_code: Option<&str>, -) -> Option { - let ident = &identifiers[id.0 as usize]; - match &ident.name { - Some(IdentifierName::Named(name)) | Some(IdentifierName::Promoted(name)) => { - return Some(bun_core::BStr::new(name.slice()).to_string()); - } - _ => {} - } - // Fall back: find another identifier with the same declaration_id that has a name. - let decl_id = ident.declaration_id; - for other in identifiers { - if other.declaration_id == decl_id { - match &other.name { - Some(IdentifierName::Named(name)) | Some(IdentifierName::Promoted(name)) => { - return Some(bun_core::BStr::new(name.slice()).to_string()); - } - _ => {} - } - } - } - // Fall back to extracting from source code using UTF-16 code unit indices. - // Babel/JS positions use UTF-16 code unit offsets, but Rust strings are UTF-8, - // so we need to convert between the two. - if let (Some(loc), Some(code)) = (loc, source_code) { - let start_utf16 = loc.start.index? as usize; - let end_utf16 = loc.end.index? as usize; - if start_utf16 < end_utf16 { - // Convert UTF-16 code unit offsets to UTF-8 byte offsets - let mut utf16_pos = 0usize; - let mut byte_start = None; - let mut byte_end = None; - for (byte_idx, ch) in code.char_indices() { - if utf16_pos == start_utf16 { - byte_start = Some(byte_idx); - } - if utf16_pos == end_utf16 { - byte_end = Some(byte_idx); - break; - } - utf16_pos += ch.len_utf16(); - } - // Handle end at the very end of string - if utf16_pos == end_utf16 && byte_end.is_none() { - byte_end = Some(code.len()); - } - if let (Some(start), Some(end)) = (byte_start, byte_end) { - let slice = &code[start..end]; - if !slice.is_empty() - && slice - .chars() - .all(|c| c.is_alphanumeric() || c == '_' || c == '$') - { - return Some(slice.to_string()); - } - } - } - } - None -} - -const MAX_FIXPOINT_ITERATIONS: usize = 100; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum TypeOfValue { - Ignored, - FromProps, - FromState, - FromPropsAndState, -} - -#[derive(Debug, Clone)] -struct DerivationMetadata { - type_of_value: TypeOfValue, - place_identifier: IdentifierId, - place_name: Option, - source_ids: crate::collections::IndexSet, - is_state_source: bool, -} - -/// Metadata about a useEffect call site. -struct EffectMetadata { - effect_func_id: FunctionId, - dep_elements: Vec, -} - -#[derive(Debug, Clone)] -struct DepElement { - identifier: IdentifierId, - loc: Option, -} - -struct ValidationContext { - /// Map from lvalue identifier to the FunctionId of function expressions - functions: HashMap, - /// Map from lvalue identifier to ArrayExpression elements (candidate deps) - candidate_dependencies: HashMap>, - derivation_cache: DerivationCache, - effects_cache: HashMap, - set_state_loads: HashMap>, - set_state_usages: HashMap>, -} - -/// A hashable key for SourceLocation to use in HashSet -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -struct LocKey { - start_line: u32, - start_col: u32, - end_line: u32, - end_col: u32, -} - -impl LocKey { - fn from_loc(loc: &Option) -> Self { - match loc { - Some(loc) => LocKey { - start_line: loc.start.line, - start_col: loc.start.column, - end_line: loc.end.line, - end_col: loc.end.column, - }, - None => LocKey { - start_line: 0, - start_col: 0, - end_line: 0, - end_col: 0, - }, - } - } -} - -#[derive(Debug, Clone)] -struct DerivationCache { - has_changes: bool, - cache: HashMap, - previous_cache: Option>, -} - -impl DerivationCache { - fn new() -> Self { - DerivationCache { - has_changes: false, - cache: HashMap::new(), - previous_cache: None, - } - } - - fn take_snapshot(&mut self) { - let mut prev = HashMap::new(); - for (key, value) in &self.cache { - prev.insert( - *key, - DerivationMetadata { - place_identifier: value.place_identifier, - place_name: value.place_name.clone(), - source_ids: value.source_ids.clone(), - type_of_value: value.type_of_value, - is_state_source: value.is_state_source, - }, - ); - } - self.previous_cache = Some(prev); - } - - fn check_for_changes(&mut self) { - let prev = match &self.previous_cache { - Some(p) => p, - None => { - self.has_changes = true; - return; - } - }; - - for (key, value) in &self.cache { - match prev.get(key) { - None => { - self.has_changes = true; - return; - } - Some(prev_value) => { - if !is_derivation_equal(prev_value, value) { - self.has_changes = true; - return; - } - } - } - } - - if self.cache.len() != prev.len() { - self.has_changes = true; - return; - } - - self.has_changes = false; - } - - fn snapshot(&mut self) -> bool { - let has_changes = self.has_changes; - self.has_changes = false; - has_changes - } - - fn add_derivation_entry( - &mut self, - derived_id: IdentifierId, - derived_name: Option, - source_ids: crate::collections::IndexSet, - type_of_value: TypeOfValue, - is_state_source: bool, - ) { - let mut final_is_source = is_state_source; - if !final_is_source { - for source_id in &source_ids { - if let Some(source_metadata) = self.cache.get(source_id) { - if source_metadata.is_state_source - && !matches!(&source_metadata.place_name, Some(IdentifierName::Named(_))) - { - final_is_source = true; - break; - } - } - } - } - - self.cache.insert( - derived_id, - DerivationMetadata { - place_identifier: derived_id, - place_name: derived_name, - source_ids, - type_of_value, - is_state_source: final_is_source, - }, - ); - } -} - -fn is_derivation_equal(a: &DerivationMetadata, b: &DerivationMetadata) -> bool { - if a.type_of_value != b.type_of_value { - return false; - } - if a.source_ids.len() != b.source_ids.len() { - return false; - } - for id in &a.source_ids { - if !b.source_ids.contains(id) { - return false; - } - } - true -} - -fn join_value(lvalue_type: TypeOfValue, value_type: TypeOfValue) -> TypeOfValue { - if lvalue_type == TypeOfValue::Ignored { - return value_type; - } - if value_type == TypeOfValue::Ignored { - return lvalue_type; - } - if lvalue_type == value_type { - return lvalue_type; - } - TypeOfValue::FromPropsAndState -} - -fn get_root_set_state( - key: IdentifierId, - loads: &HashMap>, - visited: &mut HashSet, -) -> Option { - if visited.contains(&key) { - return None; - } - visited.insert(key); - - match loads.get(&key) { - None => None, - Some(None) => Some(key), - Some(Some(parent_id)) => get_root_set_state(*parent_id, loads, visited), - } -} - -fn maybe_record_set_state_for_instr( - instr: &crate::hir::Instruction, - env: &Environment, - set_state_loads: &mut HashMap>, - set_state_usages: &mut HashMap>, -) { - let identifiers = &env.identifiers; - let types = &env.types; - - let all_lvalues = each_instruction_lvalue_ids(instr); - for &lvalue_id in &all_lvalues { - // Check if this is a LoadLocal from a known setState - if let InstructionValue::LoadLocal { place, .. } = &instr.value { - if set_state_loads.contains_key(&place.identifier) { - set_state_loads.insert(lvalue_id, Some(place.identifier)); - } else { - // Only check root setState if not a LoadLocal from a known chain - let lvalue_ident = &identifiers[lvalue_id.0 as usize]; - let lvalue_ty = &types[lvalue_ident.type_.0 as usize]; - if is_set_state_type(lvalue_ty) { - set_state_loads.insert(lvalue_id, None); - } - } - } else { - // Check if lvalue is a setState type (root setState) - let lvalue_ident = &identifiers[lvalue_id.0 as usize]; - let lvalue_ty = &types[lvalue_ident.type_.0 as usize]; - if is_set_state_type(lvalue_ty) { - set_state_loads.insert(lvalue_id, None); - } - } - - let root = get_root_set_state(lvalue_id, set_state_loads, &mut HashSet::new()); - if let Some(root_id) = root { - set_state_usages.entry(root_id).or_insert_with(|| { - let mut set = HashSet::new(); - set.insert(LocKey::from_loc(&instr.lvalue.loc)); - set - }); - } - } -} - -fn is_mutable_at( - env: &Environment, - eval_order: EvaluationOrder, - identifier_id: IdentifierId, -) -> bool { - env.identifiers[identifier_id.0 as usize] - .mutable_range - .contains(eval_order) -} - -pub fn validate_no_derived_computations_in_effects_exp( - func: &HirFunction, - env: &Environment, -) -> Result { - let identifiers = &env.identifiers; - - let mut context = ValidationContext { - functions: HashMap::new(), - candidate_dependencies: HashMap::new(), - derivation_cache: DerivationCache::new(), - effects_cache: HashMap::new(), - set_state_loads: HashMap::new(), - set_state_usages: HashMap::new(), - }; - - // Initialize derivation cache based on function type - if func.fn_type == ReactFunctionType::Hook { - for param in &func.params { - if let ParamPattern::Place(place) = param { - let name = identifiers[place.identifier.0 as usize].name.clone(); - context.derivation_cache.cache.insert( - place.identifier, - DerivationMetadata { - place_identifier: place.identifier, - place_name: name, - source_ids: crate::collections::IndexSet::new(), - type_of_value: TypeOfValue::FromProps, - is_state_source: true, - }, - ); - } - } - } else if func.fn_type == ReactFunctionType::Component { - if let Some(param) = func.params.first() { - if let ParamPattern::Place(place) = param { - let name = identifiers[place.identifier.0 as usize].name.clone(); - context.derivation_cache.cache.insert( - place.identifier, - DerivationMetadata { - place_identifier: place.identifier, - place_name: name, - source_ids: crate::collections::IndexSet::new(), - type_of_value: TypeOfValue::FromProps, - is_state_source: true, - }, - ); - } - } - } - - // Fixpoint iteration - let mut is_first_pass = true; - let mut iteration_count = 0; - loop { - context.derivation_cache.take_snapshot(); - - for (_block_id, block) in &func.body.blocks { - record_phi_derivations(block, &mut context, env); - for &instr_id in &block.instructions { - let instr = &func.instructions[instr_id.0 as usize]; - record_instruction_derivations(instr, &mut context, is_first_pass, func, env)?; - } - } - - context.derivation_cache.check_for_changes(); - is_first_pass = false; - iteration_count += 1; - assert!( - iteration_count < MAX_FIXPOINT_ITERATIONS, - "[ValidateNoDerivedComputationsInEffects] Fixpoint iteration failed to converge." - ); - - if !context.derivation_cache.snapshot() { - break; - } - } - - // Validate all effect sites - let mut errors = CompilerError::new(); - let effects_cache: Vec<(IdentifierId, FunctionId, Vec)> = context - .effects_cache - .iter() - .map(|(k, v)| (*k, v.effect_func_id, v.dep_elements.clone())) - .collect(); - - for (_key, effect_func_id, dep_elements) in &effects_cache { - validate_effect( - *effect_func_id, - dep_elements, - &mut context, - func, - env, - &mut errors, - ); - } - - Ok(errors) -} - -fn record_phi_derivations( - block: &crate::hir::BasicBlock, - context: &mut ValidationContext, - env: &Environment, -) { - let identifiers = &env.identifiers; - for phi in &block.phis { - let mut type_of_value = TypeOfValue::Ignored; - let mut source_ids: crate::collections::IndexSet = - crate::collections::IndexSet::new(); - - for (_block_id, operand) in &phi.operands { - if let Some(operand_metadata) = context.derivation_cache.cache.get(&operand.identifier) - { - type_of_value = join_value(type_of_value, operand_metadata.type_of_value); - source_ids.insert(operand.identifier); - } - } - - if type_of_value != TypeOfValue::Ignored { - let name = identifiers[phi.place.identifier.0 as usize].name.clone(); - context.derivation_cache.add_derivation_entry( - phi.place.identifier, - name, - source_ids, - type_of_value, - false, - ); - } - } -} - -fn record_instruction_derivations( - instr: &crate::hir::Instruction, - context: &mut ValidationContext, - is_first_pass: bool, - _outer_func: &HirFunction, - env: &Environment, -) -> Result<(), CompilerDiagnostic> { - let identifiers = &env.identifiers; - let types = &env.types; - let functions = &env.functions; - let lvalue_id = instr.lvalue.identifier; - - // maybeRecordSetState - maybe_record_set_state_for_instr( - instr, - env, - &mut context.set_state_loads, - &mut context.set_state_usages, - ); - - let mut type_of_value = TypeOfValue::Ignored; - let is_source = false; - let mut sources: crate::collections::IndexSet = - crate::collections::IndexSet::new(); - - match &instr.value { - InstructionValue::FunctionExpression { lowered_func, .. } => { - context.functions.insert(lvalue_id, lowered_func.func); - // Recurse into the inner function - let inner_func = &functions[lowered_func.func.0 as usize]; - for (_block_id, block) in &inner_func.body.blocks { - record_phi_derivations(block, context, env); - for &inner_instr_id in &block.instructions { - let inner_instr = &inner_func.instructions[inner_instr_id.0 as usize]; - record_instruction_derivations( - inner_instr, - context, - is_first_pass, - inner_func, - env, - )?; - } - } - } - InstructionValue::CallExpression { callee, args, .. } => { - let callee_type = &types[identifiers[callee.identifier.0 as usize].type_.0 as usize]; - if is_use_effect_hook_type(callee_type) && args.len() == 2 { - if let ( - crate::hir::PlaceOrSpread::Place(arg0), - crate::hir::PlaceOrSpread::Place(arg1), - ) = (&args[0], &args[1]) - { - let effect_function = context.functions.get(&arg0.identifier).copied(); - let deps = context - .candidate_dependencies - .get(&arg1.identifier) - .cloned(); - if let (Some(effect_func_id), Some(dep_elements)) = (effect_function, deps) { - context.effects_cache.insert( - arg0.identifier, - EffectMetadata { - effect_func_id, - dep_elements, - }, - ); - } - } - } - - // Check if lvalue is useState type - let lvalue_type = &types[identifiers[lvalue_id.0 as usize].type_.0 as usize]; - if is_use_state_type(lvalue_type) { - let name = identifiers[lvalue_id.0 as usize].name.clone(); - context.derivation_cache.add_derivation_entry( - lvalue_id, - name, - crate::collections::IndexSet::new(), - TypeOfValue::FromState, - true, - ); - return Ok(()); - } - } - InstructionValue::MethodCall { property, args, .. } => { - let prop_type = &types[identifiers[property.identifier.0 as usize].type_.0 as usize]; - if is_use_effect_hook_type(prop_type) && args.len() == 2 { - if let ( - crate::hir::PlaceOrSpread::Place(arg0), - crate::hir::PlaceOrSpread::Place(arg1), - ) = (&args[0], &args[1]) - { - let effect_function = context.functions.get(&arg0.identifier).copied(); - let deps = context - .candidate_dependencies - .get(&arg1.identifier) - .cloned(); - if let (Some(effect_func_id), Some(dep_elements)) = (effect_function, deps) { - context.effects_cache.insert( - arg0.identifier, - EffectMetadata { - effect_func_id, - dep_elements, - }, - ); - } - } - } - - // Check if lvalue is useState type - let lvalue_type = &types[identifiers[lvalue_id.0 as usize].type_.0 as usize]; - if is_use_state_type(lvalue_type) { - let name = identifiers[lvalue_id.0 as usize].name.clone(); - context.derivation_cache.add_derivation_entry( - lvalue_id, - name, - crate::collections::IndexSet::new(), - TypeOfValue::FromState, - true, - ); - return Ok(()); - } - } - InstructionValue::ArrayExpression { elements, .. } => { - let dep_elements: Vec = elements - .iter() - .filter_map(|el| match el { - ArrayElement::Place(p) => Some(DepElement { - identifier: p.identifier, - loc: p.loc, - }), - _ => None, - }) - .collect(); - context - .candidate_dependencies - .insert(lvalue_id, dep_elements); - } - _ => {} - } - - // Collect operand derivations - for (operand_id, operand_loc) in each_instruction_operand(instr, env) { - // Track setState usages - if context.set_state_loads.contains_key(&operand_id) { - let root = - get_root_set_state(operand_id, &context.set_state_loads, &mut HashSet::new()); - if let Some(root_id) = root { - if let Some(usages) = context.set_state_usages.get_mut(&root_id) { - usages.insert(LocKey::from_loc(&operand_loc)); - } - } - } - - if let Some(operand_metadata) = context.derivation_cache.cache.get(&operand_id) { - type_of_value = join_value(type_of_value, operand_metadata.type_of_value); - sources.insert(operand_id); - } - } - - if type_of_value == TypeOfValue::Ignored { - return Ok(()); - } - - // Record derivation for ALL lvalue places (including destructured variables) - for &lv_id in &each_instruction_lvalue_ids(instr) { - let name = identifiers[lv_id.0 as usize].name.clone(); - context.derivation_cache.add_derivation_entry( - lv_id, - name, - sources.clone(), - type_of_value, - is_source, - ); - } - - if matches!(&instr.value, InstructionValue::FunctionExpression { .. }) { - // Don't record mutation effects for FunctionExpressions - return Ok(()); - } - - // Handle mutable operands - for operand in each_instruction_operand_with_effect(instr, env) { - if operand.effect.is_mutable() { - if is_mutable_at(env, instr.id, operand.id) { - if let Some(existing) = context.derivation_cache.cache.get_mut(&operand.id) { - existing.type_of_value = join_value(type_of_value, existing.type_of_value); - } else { - let name = identifiers[operand.id.0 as usize].name.clone(); - context.derivation_cache.add_derivation_entry( - operand.id, - name, - sources.clone(), - type_of_value, - false, - ); - } - } - } else if matches!(operand.effect, Effect::Unknown) { - return Err(CompilerDiagnostic::new( - ErrorCategory::Invariant, - "Unexpected unknown effect", - None, - )); - } - // Freeze | Read => no-op - } - Ok(()) -} - -struct OperandWithEffect { - id: IdentifierId, - effect: Effect, -} - -/// Collects operand (IdentifierId, loc) pairs from an instruction. -/// Thin wrapper around canonical `each_instruction_operand` that maps Places to (id, loc) pairs. -fn each_instruction_operand( - instr: &crate::hir::Instruction, - env: &Environment, -) -> Vec<(IdentifierId, Option)> { - canonical_each_instruction_operand(instr, env) - .into_iter() - .map(|place| (place.identifier, place.loc)) - .collect() -} - -/// Collects operands with their effects. -/// Thin wrapper around canonical `each_instruction_operand` that maps Places to OperandWithEffect. -fn each_instruction_operand_with_effect( - instr: &crate::hir::Instruction, - env: &Environment, -) -> Vec { - canonical_each_instruction_operand(instr, env) - .into_iter() - .map(|place| OperandWithEffect { - id: place.identifier, - effect: place.effect, - }) - .collect() -} - -// ============================================================================= -// Tree building and rendering (for error messages) -// ============================================================================= - -struct TreeNode { - name: String, - type_of_value: TypeOfValue, - is_source: bool, - children: Vec, -} - -fn build_tree_node( - source_id: IdentifierId, - context: &ValidationContext, - visited: &HashSet, -) -> Vec { - let source_metadata = match context.derivation_cache.cache.get(&source_id) { - Some(m) => m, - None => return Vec::new(), - }; - - if source_metadata.is_state_source { - if let Some(IdentifierName::Named(name)) = &source_metadata.place_name { - return vec![TreeNode { - name: bun_core::BStr::new(name.slice()).to_string(), - type_of_value: source_metadata.type_of_value, - is_source: true, - children: Vec::new(), - }]; - } - } - - let mut children: Vec = Vec::new(); - let mut named_siblings: crate::collections::IndexSet = - crate::collections::IndexSet::new(); - - for child_id in &source_metadata.source_ids { - assert_ne!( - *child_id, source_id, - "Unexpected self-reference: a value should not have itself as a source" - ); - - let mut new_visited = visited.clone(); - if let Some(IdentifierName::Named(name)) = &source_metadata.place_name { - new_visited.insert(bun_core::BStr::new(name.slice()).to_string()); - } - - let child_nodes = build_tree_node(*child_id, context, &new_visited); - for child_node in child_nodes { - if !named_siblings.contains(&child_node.name) { - named_siblings.insert(child_node.name.clone()); - children.push(child_node); - } - } - } - - if let Some(IdentifierName::Named(name)) = &source_metadata.place_name { - let name = bun_core::BStr::new(name.slice()).to_string(); - if !visited.contains(&name) { - return vec![TreeNode { - name, - type_of_value: source_metadata.type_of_value, - is_source: source_metadata.is_state_source, - children, - }]; - } - } - - children -} - -fn render_tree( - node: &TreeNode, - indent: &str, - is_last: bool, - props_set: &mut crate::collections::IndexSet, - state_set: &mut crate::collections::IndexSet, -) -> String { - let prefix = format!( - "{}{}", - indent, - if is_last { - "\u{2514}\u{2500}\u{2500} " - } else { - "\u{251c}\u{2500}\u{2500} " - } - ); - let child_indent = format!("{}{}", indent, if is_last { " " } else { "\u{2502} " }); - - let mut result = format!("{}{}", prefix, node.name); - - if node.is_source { - let type_label = match node.type_of_value { - TypeOfValue::FromProps => { - props_set.insert(node.name.clone()); - "Prop" - } - TypeOfValue::FromState => { - state_set.insert(node.name.clone()); - "State" - } - _ => { - props_set.insert(node.name.clone()); - state_set.insert(node.name.clone()); - "Prop and State" - } - }; - result += &format!(" ({})", type_label); - } - - if !node.children.is_empty() { - result += "\n"; - for (index, child) in node.children.iter().enumerate() { - let is_last_child = index == node.children.len() - 1; - result += &render_tree(child, &child_indent, is_last_child, props_set, state_set); - if index < node.children.len() - 1 { - result += "\n"; - } - } - } - - result -} - -fn get_fn_local_deps( - func_id: Option, - env: &Environment, -) -> Option> { - let func_id = func_id?; - let inner = &env.functions[func_id.0 as usize]; - let mut deps: HashSet = HashSet::new(); - - for (_block_id, block) in &inner.body.blocks { - for &instr_id in &block.instructions { - let instr = &inner.instructions[instr_id.0 as usize]; - if let InstructionValue::LoadLocal { place, .. } = &instr.value { - deps.insert(place.identifier); - } - } - } - - Some(deps) -} - -fn validate_effect( - effect_func_id: FunctionId, - dependencies: &[DepElement], - context: &mut ValidationContext, - _outer_func: &HirFunction, - env: &Environment, - errors: &mut CompilerError, -) { - let identifiers = &env.identifiers; - let types = &env.types; - let functions = &env.functions; - let effect_function = &functions[effect_func_id.0 as usize]; - let mut seen_blocks: HashSet = HashSet::new(); - - struct DerivedSetStateCall { - callee_loc: Option, - callee_id: IdentifierId, - callee_identifier_name: Option, - source_ids: crate::collections::IndexSet, - } - - let mut effect_derived_set_state_calls: Vec = Vec::new(); - let mut effect_set_state_usages: HashMap> = HashMap::new(); - - // Consider setStates in the effect's dependency array as being part of effectSetStateUsages - for dep in dependencies { - let root = get_root_set_state( - dep.identifier, - &context.set_state_loads, - &mut HashSet::new(), - ); - if let Some(root_id) = root { - let mut set = HashSet::new(); - set.insert(LocKey::from_loc(&dep.loc)); - effect_set_state_usages.insert(root_id, set); - } - } - - let mut cleanup_function_deps: Option> = None; - let mut globals: HashSet = HashSet::new(); - - for (_block_id, block) in &effect_function.body.blocks { - // Check for return -> cleanup function - if let crate::hir::Terminal::Return { - value, - return_variant: ReturnVariant::Explicit, - .. - } = &block.terminal - { - let func_id = context.functions.get(&value.identifier).copied(); - cleanup_function_deps = get_fn_local_deps(func_id, env); - } - - // Skip if block has a back edge (pred not yet seen) - let has_back_edge = block.preds.iter().any(|pred| !seen_blocks.contains(pred)); - if has_back_edge { - return; - } - - for &instr_id in &block.instructions { - let instr = &effect_function.instructions[instr_id.0 as usize]; - - // Early return if any instruction derives from a ref - let lvalue_type = - &types[identifiers[instr.lvalue.identifier.0 as usize].type_.0 as usize]; - if is_use_ref_type(lvalue_type) { - return; - } - - // maybeRecordSetState for effect instructions - maybe_record_set_state_for_instr( - instr, - env, - &mut context.set_state_loads, - &mut effect_set_state_usages, - ); - - // Track setState usages for operands - for (operand_id, operand_loc) in each_instruction_operand(instr, env) { - if context.set_state_loads.contains_key(&operand_id) { - let root = get_root_set_state( - operand_id, - &context.set_state_loads, - &mut HashSet::new(), - ); - if let Some(root_id) = root { - if let Some(usages) = effect_set_state_usages.get_mut(&root_id) { - usages.insert(LocKey::from_loc(&operand_loc)); - } - } - } - } - - match &instr.value { - InstructionValue::CallExpression { callee, args, .. } => { - let callee_type = - &types[identifiers[callee.identifier.0 as usize].type_.0 as usize]; - if is_set_state_type(callee_type) && args.len() == 1 { - if let crate::hir::PlaceOrSpread::Place(arg0) = &args[0] { - let callee_metadata = - context.derivation_cache.cache.get(&callee.identifier); - - // If the setState comes from a source other than local state, skip - if let Some(cm) = callee_metadata { - if cm.type_of_value != TypeOfValue::FromState { - continue; - } - } else { - continue; - } - - let arg_metadata = context.derivation_cache.cache.get(&arg0.identifier); - if let Some(am) = arg_metadata { - // Get the user-visible identifier name, matching Babel's - // loc.identifierName. Falls back to extracting from source code. - let callee_ident_name = get_identifier_name_with_loc( - callee.identifier, - identifiers, - &callee.loc, - env.code - .as_ref() - .and_then(|c| core::str::from_utf8(c.slice()).ok()), - ); - effect_derived_set_state_calls.push(DerivedSetStateCall { - callee_loc: callee.loc, - callee_id: callee.identifier, - callee_identifier_name: callee_ident_name, - source_ids: am.source_ids.clone(), - }); - } - } - } else { - // Check if callee is from props/propsAndState -> bail - let callee_metadata = - context.derivation_cache.cache.get(&callee.identifier); - if let Some(cm) = callee_metadata { - if cm.type_of_value == TypeOfValue::FromProps - || cm.type_of_value == TypeOfValue::FromPropsAndState - { - return; - } - } - - if globals.contains(&callee.identifier) { - return; - } - } - } - InstructionValue::LoadGlobal { .. } => { - globals.insert(instr.lvalue.identifier); - for (operand_id, _) in each_instruction_operand(instr, env) { - globals.insert(operand_id); - } - } - _ => {} - } - } - seen_blocks.insert(block.id); - } - - // Emit errors for derived setState calls - for derived in &effect_derived_set_state_calls { - let root_set_state_call = get_root_set_state( - derived.callee_id, - &context.set_state_loads, - &mut HashSet::new(), - ); - if let Some(root_id) = root_set_state_call { - let effect_usage_count = effect_set_state_usages - .get(&root_id) - .map(|s| s.len()) - .unwrap_or(0); - let total_usage_count = context - .set_state_usages - .get(&root_id) - .map(|s| s.len()) - .unwrap_or(0); - if effect_set_state_usages.contains_key(&root_id) - && context.set_state_usages.contains_key(&root_id) - && effect_usage_count == total_usage_count - 1 - { - let mut props_set: crate::collections::IndexSet = - crate::collections::IndexSet::new(); - let mut state_set: crate::collections::IndexSet = - crate::collections::IndexSet::new(); - - let mut root_nodes_map: crate::collections::IndexMap = - crate::collections::IndexMap::new(); - for id in &derived.source_ids { - let nodes = build_tree_node(*id, context, &HashSet::new()); - for node in nodes { - if !root_nodes_map.contains_key(&node.name) { - root_nodes_map.insert(node.name.clone(), node); - } - } - } - let root_nodes: Vec<&TreeNode> = root_nodes_map.values().collect(); - - let trees: Vec = root_nodes - .iter() - .enumerate() - .map(|(index, node)| { - render_tree( - node, - "", - index == root_nodes.len() - 1, - &mut props_set, - &mut state_set, - ) - }) - .collect(); - - // Check cleanup function dependencies - let should_skip = if let Some(ref cleanup_deps) = cleanup_function_deps { - derived - .source_ids - .iter() - .any(|dep| cleanup_deps.contains(dep)) - } else { - false - }; - if should_skip { - return; - } - - let mut root_sources = String::new(); - if !props_set.is_empty() { - let props_list: Vec<&str> = props_set.iter().map(|s| s.as_str()).collect(); - root_sources += &format!("Props: [{}]", props_list.join(", ")); - } - if !state_set.is_empty() { - if !root_sources.is_empty() { - root_sources += "\n"; - } - let state_list: Vec<&str> = state_set.iter().map(|s| s.as_str()).collect(); - root_sources += &format!("State: [{}]", state_list.join(", ")); - } - - let description = format!( - "Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\n\ - This setState call is setting a derived value that depends on the following reactive sources:\n\n\ - {}\n\n\ - Data Flow Tree:\n\ - {}\n\n\ - See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state", - root_sources, - trees.join("\n"), - ); - - errors.push_diagnostic( - CompilerDiagnostic::new( - ErrorCategory::EffectDerivationsOfState, - "You might not need an effect. Derive values in render, not effects.", - Some(description), - ) - .with_detail(CompilerDiagnosticDetail::Error { - loc: derived.callee_loc, - message: Some( - "This should be computed during render, not in an effect".to_string(), - ), - identifier_name: derived.callee_identifier_name.clone(), - }), - ); - } - } - } -} - -// ============================================================================= -// Non-exp version: ValidateNoDerivedComputationsInEffects -// Port of ValidateNoDerivedComputationsInEffects.ts -// ============================================================================= - -/// Non-experimental version of the derived-computations-in-effects validation. /// Records errors directly on the Environment (matching TS `env.recordError()` behavior). pub(crate) fn validate_no_derived_computations_in_effects( func: &HirFunction, @@ -1250,7 +109,7 @@ pub(crate) fn validate_no_derived_computations_in_effects( // Uses ErrorDetail (flat loc format) to match TS behavior where // env.recordError(new CompilerErrorDetail({...})) is used. for (func_id, resolved_deps) in effects_to_validate { - let details = validate_effect_non_exp( + let details = validate_effect( &env.functions[func_id.0 as usize], &resolved_deps, &env.identifiers, @@ -1263,7 +122,7 @@ pub(crate) fn validate_no_derived_computations_in_effects( Ok(()) } -fn validate_effect_non_exp( +fn validate_effect( effect_func: &HirFunction, effect_deps: &[IdentifierId], ids: &[Identifier], @@ -1335,7 +194,7 @@ fn validate_effect_non_exp( | InstructionValue::CallExpression { .. } | InstructionValue::MethodCall { .. } => { let mut aggregate: HashSet = HashSet::new(); - for operand in non_exp_value_operands(&instr.value) { + for operand in value_operands(&instr.value) { if let Some(deps) = dep_values.get(&operand) { for d in deps { aggregate.insert(*d); @@ -1410,15 +269,15 @@ fn validate_effect_non_exp( } /// Collects operand IdentifierIds for a subset of instruction variants used -/// by `validate_effect_non_exp`. +/// by `validate_effect`. /// /// NOTE: This intentionally does NOT use the canonical `each_instruction_value_operand` -/// because: (1) `validate_effect_non_exp` only matches specific variants +/// because: (1) `validate_effect` only matches specific variants /// (ComputedLoad, PropertyLoad, BinaryExpression, TemplateLiteral, CallExpression, /// MethodCall), so FunctionExpression/ObjectMethod context handling is unnecessary; /// and (2) the caller does not have access to `env` which the canonical function requires /// for resolving function expression context captures. -fn non_exp_value_operands(value: &InstructionValue) -> Vec { +fn value_operands(value: &InstructionValue) -> Vec { match value { InstructionValue::ComputedLoad { object, property, .. diff --git a/src/runtime/api/bun/h2_frame_parser.rs b/src/runtime/api/bun/h2_frame_parser.rs index bc47259df012..acf47ac630bd 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,31 @@ 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)] +// HTTP/2 SETTINGS values, one field per standard parameter. #[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 +416,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 +461,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. @@ -1183,7 +1023,7 @@ impl TxFrameTracker { let header = FrameHeader::decode(&self.header); self.header_len = 0; self.remaining = header.length; - // PUSH_PROMISE is not a FrameType variant (the inbound path matches it raw too). + // PUSH_PROMISE is not a FrameType variant. const PUSH_PROMISE: u8 = 0x05; if header.type_ == FrameType::HTTP_FRAME_HEADERS as u8 || header.type_ == PUSH_PROMISE @@ -1274,18 +1114,10 @@ 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 window_size: Cell, - // used window size for the connection - used_window_size: Cell, // remote Window limits the upload of data // remote window size for the connection @@ -1337,11 +1169,7 @@ 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, - preface_received_len: Cell, // we buffer requests until we get the first settings ACK write_buffer: JsCell>, write_buffer_offset: Cell, @@ -1379,8 +1207,7 @@ pub struct H2FrameParser { padding_strategy: Cell, // ---- from-scratch rewrite engine (src/runtime/api/bun/h2) ---- - // The fields above are the legacy frame state being retired; read()/host functions will route - // through `engine` instead. `None` until configured with is_server + settings. + // `None` until configured with is_server + settings. engine: core::cell::RefCell>, /// Unconsumed inbound tail (the engine holds no reassembly buffer — design B): bytes after the /// last complete frame are kept here and prepended to the next read(). @@ -1516,16 +1343,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, @@ -1533,8 +1350,6 @@ pub struct Stream { weight: u16, // current window size for the stream window_size: u64, - // used window size for the stream - used_window_size: u64, // remote window size for the stream remote_window_size: u64, // remote used window size for the stream @@ -2063,12 +1878,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, @@ -2077,7 +1886,6 @@ impl Stream { // which is what stream.state.weight reports when no priority was signaled. weight: 16, window_size: initial_window_size as u64, - used_window_size: 0, remote_window_size: remote_window_size as u64, remote_used_window_size: 0, signal: None, @@ -2212,8 +2020,6 @@ impl AbortListener for SignalRef { } } -type HeaderValue = lshpack::DecodeResult; - // ────────────────────────────────────────────────────────────────────────── // H2FrameParser impl — core methods // ────────────────────────────────────────────────────────────────────────── @@ -2255,15 +2061,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], @@ -2283,97 +2080,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. @@ -2692,20 +2398,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, @@ -2836,33 +2528,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. @@ -3688,42 +3353,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; @@ -3761,1878 +3390,103 @@ extern "C" fn on_auto_flush_trampoline(ctx: *mut c_void) -> bool { // are inherent methods now.) // ────────────────────────────────────────────────────────────────────────── -// H2FrameParser impl — frame handlers +// H2FrameParser impl — stream registry // ────────────────────────────────────────────────────────────────────────── 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, - ); + 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) + } + + /// 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; } - 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, - }); + // already exists + if let Some(stream) = self.streams.get().get(&stream_identifier).copied() { + return Some(stream); } - Some(Payload { - data_ptr: payload.as_ptr(), - data_len: payload.len(), - end, - }) - } + 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); + } - 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; + // 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); + }; + let Some(ctx_value) = JSH2FrameParser::Gc::context.get(this_value) else { + return Some(stream); + }; + let Some(callback) = JSH2FrameParser::Gc::onStreamStart.get(this_value) else { + return Some(stream); + }; + + let global = self.handlers.get().global(); + // A prior frame's callback can drain microtasks that tear the worker + // down (worker.terminate()); skip rather than calling JS with the + // termination exception pending. + if global.has_exception() { + return Some(stream); + } + match callback.call( + &global, + ctx_value, + &[ctx_value, JSValue::js_number(stream_identifier as f64)], + ) { + Err(err) => global.report_active_exception_as_unhandled(err), + Ok(returned) => { + // streamStart returns the JS stream it created; storing it here saves the + // setStreamContext host call the JS layer used to make per stream. + if returned.is_object() { + self.sctx.with_mut(|m| { + m.insert(stream_identifier, StrongOptional::create(returned, &global)); + }); + // SAFETY: stream is *mut Stream from self.streams; valid while the map + // entry exists + unsafe { (*stream).set_context(returned, &global) }; } - self.remote_window_size.set(next); } - 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() + Some(stream) } - /// 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 to_writer(&self) -> DirectWriterStruct { + DirectWriterStruct { + writer: bun_ptr::BackRef::new(self), } } - - /// 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); - } - 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(); - - let mut sensitive_headers: JSValue = JSValue::UNDEFINED; - let mut malformed = false; - - // 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() - } - - 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)); - - let Some(this_value) = self.strong_this.get().try_get() else { - return Some(stream); - }; - let Some(ctx_value) = JSH2FrameParser::Gc::context.get(this_value) else { - return Some(stream); - }; - let Some(callback) = JSH2FrameParser::Gc::onStreamStart.get(this_value) else { - return Some(stream); - }; - - let global = self.handlers.get().global(); - // A prior frame's callback can drain microtasks that tear the worker - // down (worker.terminate()); skip rather than calling JS with the - // termination exception pending. Same guard as read_bytes(). - if global.has_exception() { - return Some(stream); - } - match callback.call( - &global, - ctx_value, - &[ctx_value, JSValue::js_number(stream_identifier as f64)], - ) { - Err(err) => global.report_active_exception_as_unhandled(err), - Ok(returned) => { - // streamStart returns the JS stream it created; storing it here saves the - // setStreamContext host call the JS layer used to make per stream. - if returned.is_object() { - self.sctx.with_mut(|m| { - m.insert(stream_identifier, StrongOptional::create(returned, &global)); - }); - // SAFETY: stream is *mut Stream from self.streams; valid while the map - // entry exists - unsafe { (*stream).set_context(returned, &global) }; - } - } - } - 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), - } - } -} +} /// Bridge the rewrite engine's `Settings` to the JS settings object via the legacy wire payload. fn rewrite_settings_to_js(s: &crate::api::h2::settings::Settings, global: GlobalRef) -> JSValue { @@ -5644,7 +3498,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) } @@ -5980,10 +3833,9 @@ 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 + // §6.9.2: when the peer's INITIAL_WINDOW_SIZE grows, raise the // send window of streams opened before its SETTINGS arrived (a client's first request is // typically sent before the server's SETTINGS lands), then resume queued sends. for (_, item) in self.streams.get().iter() { @@ -6036,9 +3888,8 @@ impl crate::api::h2::connection::Sink for H2FrameParser { ); return; } - // Balance the increment from send_ping: the legacy inbound path decremented this in - // handle_ping; the engine path must do the same or the outstanding-ping limit trips - // on long-lived sessions. + // Balance the increment from send_ping, or the outstanding-ping + // limit trips on long-lived sessions. self.out_standing_pings .set(self.out_standing_pings.get().saturating_sub(1)); } @@ -6265,8 +4116,7 @@ impl crate::api::h2::connection::Sink for H2FrameParser { fn on_stream_end(&self, stream_id: u32, state: u8) { // The engine only sees the inbound half while outbound flows through the legacy path, so it // can't know the local side already sent END_STREAM. Combine with the legacy stream's local - // state: remote-closed (6) on a stream whose local half is closed (5/7) is fully CLOSED (7), - // mirroring the legacy handle_data/headers END_STREAM logic. + // state: remote-closed (6) on a stream whose local half is closed (5/7) is fully CLOSED (7). let mut effective = state; if let Some(stream) = self.streams.get().get(&stream_id).copied() { // SAFETY: stream is *mut Stream from self.streams; valid while the map entry exists @@ -6711,11 +4561,6 @@ impl H2FrameParser { .throw_invalid_arguments(format_args!("Expected windowSize to be a number"))); } let window_size_value: u32 = window_size.to_u32(); - if this.used_window_size.get() > window_size_value as u64 { - return Err(global_object.throw_invalid_arguments(format_args!( - "Expected windowSize to be greater than usedWindowSize" - ))); - } let old_window_size = this.window_size.get(); this.window_size.set(window_size_value as u64); if this.local_settings.get().initial_window_size < window_size_value { @@ -6752,9 +4597,6 @@ impl H2FrameParser { for (_, item) in this.streams.get().iter() { // SAFETY: item is &*mut Stream from streams.iter(); the boxed Stream outlives the iteration let stream = unsafe { &mut **item }; - if stream.used_window_size > window_size_value as u64 { - continue; - } stream.window_size = window_size_value as u64; } Ok(JSValue::UNDEFINED) @@ -6798,7 +4640,7 @@ impl H2FrameParser { result.put( global_object, b"effectiveRecvDataLength", - JSValue::js_number((this.window_size.get() - this.used_window_size.get()) as f64), + JSValue::js_number(this.window_size.get() as f64), ); result.put( global_object, @@ -9744,11 +7586,7 @@ 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), remote_used_window_size: Cell::new(0), max_header_list_pairs: Cell::new(128), @@ -9772,9 +7610,7 @@ 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), - preface_received_len: Cell::new(0), write_buffer: JsCell::new(Vec::::default()), write_buffer_offset: Cell::new(0), outbound_queue_size: Cell::new(0), @@ -10004,10 +7840,6 @@ 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()); 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. @@ -10066,7 +7898,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/src/runtime/error.rs b/src/runtime/error.rs index c10ffc0125cc..9acc94af3db7 100644 --- a/src/runtime/error.rs +++ b/src/runtime/error.rs @@ -300,8 +300,6 @@ pub enum Error { FailedToGetTempPath, #[error("UnexpectedCreatingStdin")] UnexpectedCreatingStdin, - #[error("UnableToDecode")] - UnableToDecode, #[error("UnableToEncode")] UnableToEncode, #[error("SocketClosed")] @@ -731,7 +729,6 @@ impl Error { Self::CouldntReadCurrentDirectory => "CouldntReadCurrentDirectory", Self::FailedToGetTempPath => "FailedToGetTempPath", Self::UnexpectedCreatingStdin => "UnexpectedCreatingStdin", - Self::UnableToDecode => "UnableToDecode", Self::UnableToEncode => "UnableToEncode", Self::SocketClosed => "SocketClosed", Self::InvalidHeaderName => "InvalidHeaderName", diff --git a/test/bundler/transpiler/react-compiler-fixtures.test.ts b/test/bundler/transpiler/react-compiler-fixtures.test.ts index 48eb32cc011f..7f9278dfc691 100644 --- a/test/bundler/transpiler/react-compiler-fixtures.test.ts +++ b/test/bundler/transpiler/react-compiler-fixtures.test.ts @@ -83,6 +83,10 @@ const IGNORED_PRAGMAS = new Set([ // current harness ignores them. "xonly", "Pass", + // The experimental derived-computations validation was removed; the pragma + // is parsed but applies nothing. + "validateNoDerivedComputationsInEffectsExp", + "validateNoDerivedComputationsInEffects_exp", ]); // Pragmas Bun's `parse_fixture_pragmas` (src/react_compiler/program.rs) reads @@ -115,8 +119,6 @@ const HANDLED_PRAGMAS = new Set([ "enableUseKeyedState", "validateNoSetStateInEffects", "validateNoDerivedComputationsInEffects", - "validateNoDerivedComputationsInEffectsExp", - "validateNoDerivedComputationsInEffects_exp", "validateNoJsxInTryStatements", "validateNoJSXInTryStatements", "validateStaticComponents", diff --git a/test/internal/source-lints/dead-symbols-react-compiler-h2.test.ts b/test/internal/source-lints/dead-symbols-react-compiler-h2.test.ts new file mode 100644 index 000000000000..2feaca4e30dd --- /dev/null +++ b/test/internal/source-lints/dead-symbols-react-compiler-h2.test.ts @@ -0,0 +1,120 @@ +// Guards against reintroduction of symbols removed as dead code from +// bun_react_compiler, the node:http2 frame parser, and bun_exe_format. +// Each entry was verified to have zero references across src/, scripts/, +// test/, and build/debug/codegen/ output before deletion; the Rust removals +// were additionally confirmed by `rustc --force-warn dead_code` reaching a +// warning-free fixpoint on the touched crates. +// +// 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. + +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 resurrected(checks: Array<[string, RegExp]>): string[] { + const hits: string[] = []; + for (const [file, re] of checks) { + if (re.test(src(file))) { + hits.push(`${file}: ${re}`); + } + } + return hits; +} + +test("dead react_compiler symbols do not reappear", () => { + const checks: Array<[string, RegExp]> = [ + // The experimental variant of the derived-computations-in-effects + // validation (~1.1k lines). The pipeline only ever invoked the non-exp + // version; the _exp config flag is parsed from pragmas but never read. + [ + "src/react_compiler/validation/validate_no_derived_computations_in_effects.rs", + /validate_no_derived_computations_in_effects_exp/, + ], + // The write-only env-config flag and pragma arm that fed it. + ["src/react_compiler/hir/environment_config.rs", /validate_no_derived_computations_in_effects_exp/], + ["src/react_compiler/program.rs", /validate_no_derived_computations_in_effects_exp/], + // Back-compat alias for a previous parser-hook API; nothing used it. + ["src/react_compiler/program.rs", /\bSymbolHost\b/], + ["src/react_compiler/lib.rs", /\bSymbolHost\b/], + // Arena box alias with zero uses (HirVec is the one HIR actually uses). + ["src/react_compiler/hir/mod.rs", /\bHirBox\b/], + // Type predicate whose only callers were in the removed _exp validation. + ["src/react_compiler/hir/mod.rs", /\bis_use_state_type\b/], + ]; + expect(resurrected(checks)).toEqual([]); +}); + +test("dead h2_frame_parser inbound-path symbols do not reappear", () => { + // The pre-engine inbound frame-handling path: once rewrite_read() routed all + // inbound bytes through crate::api::h2::Connection, these handlers and their + // wire-decoding helpers and buffering state became unreachable. + const file = "src/runtime/api/bun/h2_frame_parser.rs"; + const checks: Array<[string, RegExp]> = [ + [file, /fn handle_incomming_payload\b/], + [file, /fn handle_window_update_frame\b/], + [file, /fn handle_unknown_frame\b/], + [file, /fn handle_push_promise_frame\b/], + [file, /fn decode_header_block\b/], + [file, /fn handle_data_frame\b/], + [file, /fn handle_go_away_frame\b/], + [file, /fn handle_origin_frame\b/], + [file, /fn handle_altsvc_frame\b/], + [file, /fn handle_rst_stream_frame\b/], + [file, /fn handle_ping_frame\b/], + [file, /fn handle_priority_frame\b/], + [file, /fn handle_continuation_frame\b/], + [file, /fn finish_headers_end_stream\b/], + [file, /fn handle_headers_frame\b/], + [file, /fn handle_settings_frame\b/], + [file, /fn lookup_inbound_stream\b/], + [file, /fn read_bytes\b/], + [file, /fn dispatch_frame\b/], + // Decode-direction wire helpers only the handlers above used. + [file, /fn u32_from_bytes\b/], + [file, /enum SettingsFlags\b/], + [file, /fn get_http2_common_string\b/], + [file, /JSC__JSGlobalObject__getHTTP2CommonString/], + [file, /type HeaderValue\b/], + [file, /fn dispatch_with_3_extra\b/], + [file, /fn send_settings_ack\b/], + [file, /fn adjust_window_size\b/], + [file, /fn increment_window_size_if_needed\b/], + [file, /struct Payload\b/], + // Write-only parser/stream state the dead handlers maintained. + [file, /\bexpecting_continuation\b/], + [file, /\bpreface_received_len\b/], + [file, /\bpending_header_block\b/], + [file, /\bis_waiting_more_headers\b/], + [file, /\bread_buffer\b/], + // \b does not match inside remote_used_window_size (underscore is a word + // character), so this only catches the removed standalone fields. + [file, /\bused_window_size\b/], + // Wire-format leftovers: the SETTINGS id newtype and the packed-layout + // artifacts lost their last consumer with the inbound path. + [file, /\bSettingsType\b/], + [file, /_header_table_size_type/], + // The removed decode() was the sole constructor of this error variant. + ["src/runtime/error.rs", /\bUnableToDecode\b/], + // Wire-decode leftovers that escaped the dead_code lint (pub struct, + // trait impls): the parser's own SettingsPayloadUnit copy and the + // bytemuck impls whose only consumer was the removed write(). + [file, /\bSettingsPayloadUnit\b/], + [file, /bytemuck::(Zeroable|Pod) for FullSettingsPayload/], + ]; + expect(resurrected(checks)).toEqual([]); +}); + +test("dead exe_format symbols do not reappear", () => { + const checks: Array<[string, RegExp]> = [ + // pe::Error::InsufficientSpace was never constructed. + ["src/exe_format/pe.rs", /\bInsufficientSpace\b/], + ]; + expect(resurrected(checks)).toEqual([]); +});