Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/http/lshpack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ impl HPACK {
lshpack_wrapper_enc_set_max_capacity(self, max_capacity as c_uint);
}

/// Sets the limit on the peer's RFC 7541 §6.3 size updates and the current
/// table size, both. Evicts entries to fit.
Comment thread
robobun marked this conversation as resolved.
pub fn set_decoder_max_capacity(&mut self, max_capacity: u32) {
lshpack_wrapper_dec_set_max_capacity(self, max_capacity as c_uint);
}

// Raw `*mut HPACK` teardown is subsumed by the
// safe [`HpackHandle`] RAII wrapper below — every owner holds an
// `HpackHandle`, so the raw destructor is private to `HpackHandle::drop`.
Expand Down Expand Up @@ -209,6 +215,7 @@ unsafe extern "C" {
// Only precondition is a valid non-null `*HPACK`; `&mut HPACK` (ABI-identical
// thin pointer) discharges it at the type level, so this is `safe fn`.
safe fn lshpack_wrapper_enc_set_max_capacity(self_: &mut HPACK, max_capacity: c_uint);
safe fn lshpack_wrapper_dec_set_max_capacity(self_: &mut HPACK, max_capacity: c_uint);
// Frees `self_` (lshpack_{enc,dec}_cleanup + mi_free) — ownership transfer,
// so this keeps its raw-pointer signature and caller-side safety obligation.
fn lshpack_wrapper_deinit(self_: *mut HPACK);
Expand Down
5 changes: 5 additions & 0 deletions src/jsc/bindings/c-bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,11 @@ void lshpack_wrapper_enc_set_max_capacity(lshpack_wrapper* self, unsigned max_ca
lshpack_enc_set_max_capacity(&self->enc, max_capacity);
}

void lshpack_wrapper_dec_set_max_capacity(lshpack_wrapper* self, unsigned max_capacity)
{
lshpack_dec_set_max_capacity(&self->dec, max_capacity);
}

void lshpack_wrapper_deinit(lshpack_wrapper* self)
{
lshpack_dec_cleanup(&self->dec);
Expand Down
29 changes: 18 additions & 11 deletions src/runtime/api/bun/h2/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ pub trait Sink {
}
/// A SETTINGS entry with an id outside the standard registry (node's remoteCustomSettings).
fn on_remote_custom_setting(&self, _id: u16, _value: u32) {}
/// One SETTINGS_HEADER_TABLE_SIZE entry from the peer, called per entry in wire order.
fn on_remote_header_table_size(&self, _size: u32) {}
/// One decoded header field. `name`/`value` alias a shared buffer — copy before returning.
fn on_header(&self, _stream_id: u32, _name: &[u8], _value: &[u8], _never_index: bool) {}
/// The header block for `stream_id` is complete. `end_stream` = the HEADERS carried END_STREAM.
Expand Down Expand Up @@ -289,6 +291,7 @@ pub struct Connection {

/// Scratch buffer for the outbound HPACK-encoded header block.
enc_buf: Vec<u8>,
enc_announced: hpack::AnnouncedAt,
/// Reusable scratch for end-of-batch window replenishment (stream id, increment).
replenish_buf: Vec<(u32, u32)>,
/// Reused buffer for evicting closed streams after each receive pass (no per-call allocation).
Expand Down Expand Up @@ -317,14 +320,15 @@ impl Connection {
max_settings: 32,
send_window: SendWindow::new(wire::DEFAULT_WINDOW_SIZE),
recv_window: RecvWindow::new(wire::DEFAULT_WINDOW_SIZE),
hpack: hpack::Coder::new(local.header_table_size),
hpack: hpack::Coder::new(),
streams: HashMap::new(),
header_block_in_flight: None,
header_block: Vec::new(),
data_in_flight: None,
terminated: false,
obq_ack_pending: 0,
enc_buf: Vec::new(),
enc_announced: Default::default(),
replenish_buf: Vec::new(),
evict_buf: Vec::new(),
preface_received: 0,
Expand Down Expand Up @@ -680,6 +684,8 @@ impl Connection {
// The peer has acknowledged this submission: header-list enforcement may now use the
// limit it carried.
self.enforced_max_header_list_size = acked.settings.max_header_list_size;
self.hpack
.set_acked_header_table_size(acked.settings.header_table_size);
sink.on_local_settings(&acked.settings);
return false;
}
Expand All @@ -698,7 +704,6 @@ impl Connection {
self.send_go_away(sink, code, b"SETTINGS value out of range");
return true;
}
let old_table = self.remote_settings.header_table_size;
let old_initial_window = self.remote_settings.initial_window_size;
let mut i = 0;
while i + 6 <= payload.len() {
Expand All @@ -724,16 +729,16 @@ impl Connection {
return true;
}
self.remote_settings.apply(sid, value);
// Per entry, not once per frame: the peer's decoder evicts at each (RFC 7541 §4.2).
if sid == SettingId::HeaderTableSize {
self.hpack.set_peer_header_table_size(value);
sink.on_remote_header_table_size(value);
}
} else {
sink.on_remote_custom_setting(id, value);
}
i += 6;
}
// The peer's HEADER_TABLE_SIZE governs OUR encoder; queue a 6.3 size update.
if self.remote_settings.header_table_size != old_table {
self.hpack
.queue_encoder_capacity(self.remote_settings.header_table_size);
}
// 6.9.2: a change to SETTINGS_INITIAL_WINDOW_SIZE adjusts every non-closed stream's send
// window by the delta (the connection window is not affected).
if self.remote_settings.initial_window_size != old_initial_window {
Expand Down Expand Up @@ -1211,6 +1216,8 @@ impl Connection {
}
sink.on_header(target, h.name, h.value, h.never_index);
}
// Size updates and no field, for example empty trailers. lshpack applied them.
Err(_) if off == 0 && self.hpack.is_size_update_only(&block) => break,
Err(_) => {
// §4.3: a header-block decoding error is a connection COMPRESSION_ERROR.
self.send_go_away(sink, ErrorCode::CompressionError, b"HPACK decode error");
Expand Down Expand Up @@ -1780,9 +1787,7 @@ impl Connection {
/// Begin a new outbound header block. Emits any pending §6.3 dynamic-table size update first.
pub fn begin_header_block(&mut self) {
self.enc_buf.clear();
let mut tmp = [0u8; hpack::MAX_SIZE_UPDATE_BYTES];
let n = self.hpack.take_pending_size_update(&mut tmp, 0);
self.enc_buf.extend_from_slice(&tmp[..n]);
self.enc_announced = self.hpack.write_pending_size_update(&mut self.enc_buf);
}

/// HPACK-encode one header field into the current block. Returns false on encode failure.
Expand All @@ -1808,6 +1813,7 @@ impl Connection {
/// it exceeds the peer's max frame size (§4.3/§6.10), and advance the send-side stream state.
pub fn send_header_block(&mut self, sink: &impl Sink, stream_id: u32, end_stream: bool) {
let block = std::mem::take(&mut self.enc_buf);
self.hpack.size_update_committed(self.enc_announced);
let max = (self.remote_settings.max_frame_size as usize).max(1);
let total = block.len();

Expand Down Expand Up @@ -1939,6 +1945,7 @@ impl Connection {
/// promised request headers staged via begin_header_block/encode_header (RFC 9113 §6.6).
pub fn send_push_promise(&mut self, sink: &impl Sink, parent_id: u32, promised_id: u32) {
let block = std::mem::take(&mut self.enc_buf);
self.hpack.size_update_committed(self.enc_announced);
let max = (self.remote_settings.max_frame_size as usize).max(5);

// First frame: PUSH_PROMISE = 4-byte promised id + (head of) the header block.
Expand Down Expand Up @@ -2067,7 +2074,7 @@ mod tests {

/// Encode a header block with a standalone coder (mirrors a real peer's encoder).
fn encode_block(pairs: &[(&[u8], &[u8])]) -> Vec<u8> {
let mut coder = hpack::Coder::new(4096);
let mut coder = hpack::Coder::new();
let mut buf = vec![0u8; 4096];
let mut off = 0usize;
for (name, value) in pairs {
Expand Down
172 changes: 123 additions & 49 deletions src/runtime/api/bun/h2/hpack.rs
Original file line number Diff line number Diff line change
@@ -1,53 +1,95 @@
//! HPACK coder (RFC 7541) over the lshpack binding — the only reused piece in the rewrite.
//!
//! Centralizes the dynamic-table-size-update handling: when the peer changes
//! SETTINGS_HEADER_TABLE_SIZE, the encoder capacity is lowered AND a §6.3 Dynamic Table Size
//! Update opcode is emitted at the start of the next header block so the peer's decoder evicts in
//! lockstep. Decode results alias a shared buffer and MUST be copied before the next call
//! (see lshpack.rs).
//! Centralizes the dynamic-table-size handling: the encoder follows the PEER's
//! SETTINGS_HEADER_TABLE_SIZE and announces each change with a §6.3 Dynamic Table Size Update at
//! the start of the next header block. The decoder follows OUR setting once the peer ACKs it.
//! Decode results alias a shared buffer and MUST be copied before the next call (see lshpack.rs).
Comment thread
robobun marked this conversation as resolved.

#![allow(dead_code)]

use bun_http::lshpack::{DecodeResult, HpackError, HpackHandle};

/// RFC 7541 §6.3: a Dynamic Table Size Update integer never needs more than 6 bytes for a u32.
pub const MAX_SIZE_UPDATE_BYTES: usize = 6;
/// RFC 9113 §6.5.2: the initial SETTINGS_HEADER_TABLE_SIZE of both sides.
pub const DEFAULT_HEADER_TABLE_SIZE: u32 = 4096;

/// A peer must not decide how much header history this side retains. Node's default
/// `maxDeflateDynamicTableSize`.
Comment thread
robobun marked this conversation as resolved.
pub const MAX_ENCODER_TABLE_SIZE: u32 = 4096;

/// What a header block announced when it was opened. See [`Coder::size_update_committed`].
#[derive(Clone, Copy, Default)]
pub struct AnnouncedAt(u32);

pub struct Coder {
hpack: HpackHandle,
enc_capacity: u32,
/// A capacity change requested by the peer's SETTINGS_HEADER_TABLE_SIZE, applied + announced at
/// the start of the next encoded header block. `None` = nothing pending.
pending_enc_capacity: Option<u32>,
/// `Some` while a capacity change is not announced: the smallest capacity since the last
/// block that was sent. RFC 7541 §4.2 wants that minimum signaled before the final value.
Comment thread
robobun marked this conversation as resolved.
unannounced_min: Option<u32>,
/// Counts the changes of `unannounced_min`.
generation: u32,
/// Our SETTINGS_HEADER_TABLE_SIZE that the peer ACKed last.
dec_capacity: u32,
}

impl Coder {
pub fn new(max_capacity: u32) -> Self {
pub fn new() -> Self {
Coder {
hpack: HpackHandle::new(max_capacity),
enc_capacity: max_capacity,
pending_enc_capacity: None,
hpack: HpackHandle::new(DEFAULT_HEADER_TABLE_SIZE),
enc_capacity: DEFAULT_HEADER_TABLE_SIZE,
unannounced_min: None,
generation: 0,
dec_capacity: DEFAULT_HEADER_TABLE_SIZE,
}
}

/// Call on the SETTINGS ACK, not when the SETTINGS frame is sent: until the ACK the peer's
/// encoder still works against the previous value.
Comment thread
robobun marked this conversation as resolved.
pub fn set_acked_header_table_size(&mut self, size: u32) {
if size == self.dec_capacity {
return;
}
self.hpack.set_decoder_max_capacity(size);
self.dec_capacity = size;
}

pub fn encoder_capacity(&self) -> u32 {
self.enc_capacity
}

/// Schedule an encoder capacity change from a received SETTINGS_HEADER_TABLE_SIZE. Applied
/// lazily so the §6.3 size-update opcode is emitted inside the next header block.
pub fn queue_encoder_capacity(&mut self, capacity: u32) {
if capacity == self.enc_capacity && self.pending_enc_capacity.is_none() {
/// Call for every SETTINGS_HEADER_TABLE_SIZE entry of the peer, in wire order.
pub fn set_peer_header_table_size(&mut self, size: u32) {
let capacity = size.min(MAX_ENCODER_TABLE_SIZE);
if capacity == self.enc_capacity && self.unannounced_min.is_none() {
return;
}
Comment thread
robobun marked this conversation as resolved.
self.pending_enc_capacity = Some(capacity);
self.hpack.set_encoder_max_capacity(capacity);
self.enc_capacity = capacity;
self.unannounced_min = Some(match self.unannounced_min {
Some(min) => min.min(capacity),
None => capacity,
});
self.generation = self.generation.wrapping_add(1);
}

/// If a capacity change is pending, apply it and write the §6.3 size-update opcode into `dst` at
/// `offset`. Returns bytes written (0 if none). `dst[offset..]` needs >= MAX_SIZE_UPDATE_BYTES.
pub fn take_pending_size_update(&mut self, dst: &mut [u8], offset: usize) -> usize {
let Some(cap) = self.pending_enc_capacity.take() else {
return 0;
};
self.hpack.set_encoder_max_capacity(cap);
self.enc_capacity = cap;
write_table_size_update(dst, offset, cap)
/// Call at the start of every outbound header block. The update stays pending until
/// [`Self::size_update_committed`], so a block that is built but never sent does not lose it.
Comment thread
robobun marked this conversation as resolved.
pub fn write_pending_size_update(&self, block: &mut Vec<u8>) -> AnnouncedAt {
if let Some(min) = self.unannounced_min {
if min < self.enc_capacity {
write_table_size_update(block, min);
}
write_table_size_update(block, self.enc_capacity);
}
AnnouncedAt(self.generation)
}

/// User JS runs while a block is built and can deliver a peer SETTINGS frame. A change that
/// arrived after the block was opened is not in the block, so it stays pending.
Comment thread
robobun marked this conversation as resolved.
pub fn size_update_committed(&mut self, announced: AnnouncedAt) {
if announced.0 == self.generation {
self.unannounced_min = None;
}
}

#[inline]
Expand All @@ -67,27 +109,60 @@ impl Coder {
pub fn decode(&mut self, src: &[u8]) -> Result<DecodeResult, HpackError> {
self.hpack.decode(src)
}

/// True when `block` is only §6.3 size updates within the ACKed limit: a valid block with no
/// field. lshpack applies such updates and then fails [`Self::decode`] because no field
/// follows, so ask this after that failure.
Comment thread
robobun marked this conversation as resolved.
pub fn is_size_update_only(&self, block: &[u8]) -> bool {
let mut rest = block;
if rest.is_empty() {
return false;
}
while let Some((&first, tail)) = rest.split_first() {
if first & 0xe0 != 0x20 {
return false;
}
rest = tail;
let mut value = u64::from(first & 0x1f);
if value == 0x1f {
// Up to 4 continuation bytes: the range lshpack accepts without further checks.
let mut shift = 0;
loop {
let Some((&byte, tail)) = rest.split_first() else {
return false;
};
rest = tail;
if shift > 21 {
return false;
}
value += u64::from(byte & 0x7f) << shift;
shift += 7;
if byte & 0x80 == 0 {
break;
}
}
}
if value > u64::from(self.dec_capacity) {
return false;
}
}
true
}
}

/// RFC 7541 §5.1 + §6.3: encode `value` as a 5-bit-prefix integer with the `001` pattern (0x20)
/// into `dst[offset..]`. Returns bytes written.
fn write_table_size_update(dst: &mut [u8], offset: usize, value: u32) -> usize {
let mut i = offset;
/// RFC 7541 §5.1 + §6.3: append `value` as a 5-bit-prefix integer with the `001` pattern (0x20).
fn write_table_size_update(block: &mut Vec<u8>, value: u32) {
if value < 31 {
dst[i] = 0x20 | value as u8;
return 1;
block.push(0x20 | value as u8);
return;
}
dst[i] = 0x20 | 31;
i += 1;
block.push(0x20 | 31);
let mut rest = value - 31;
while rest >= 128 {
dst[i] = (rest as u8) | 0x80;
i += 1;
block.push((rest as u8) | 0x80);
rest >>= 7;
}
dst[i] = rest as u8;
i += 1;
i - offset
block.push(rest as u8);
}

#[cfg(test)]
Expand All @@ -96,17 +171,16 @@ mod tests {

#[test]
fn size_update_small() {
let mut buf = [0u8; 6];
assert_eq!(write_table_size_update(&mut buf, 0, 30), 1);
assert_eq!(buf[0], 0x20 | 30);
let mut block = Vec::new();
write_table_size_update(&mut block, 30);
assert_eq!(block, [0x20 | 30]);
}

#[test]
fn size_update_large() {
let mut buf = [0u8; 6];
// 4096 = 31 + 4065; 4065 = 0b111_1110_0001 -> 0xE1, 0x1F
let n = write_table_size_update(&mut buf, 0, 4096);
assert_eq!(buf[0], 0x3f); // 0x20 | 31
assert!(n >= 2);
let mut block = Vec::new();
// 4096 = 31 + 4065; 4065 = 0b11111_1100001 -> 0xE1, 0x1F
write_table_size_update(&mut block, 4096);
assert_eq!(block, [0x3f, 0xe1, 0x1f]);
}
}
Loading
Loading