bun_http 0.1.0

A Rust-native programmable browser runtime built on Servo and SpiderMonkey
use core::ffi::{c_int, c_uint, c_void};

#[repr(C)]
struct lshpack_header {
    name: *const u8,
    name_len: usize,
    value: *const u8,
    value_len: usize,
    never_index: bool,
    hpack_index: u16,
}

impl Default for lshpack_header {
    fn default() -> Self {
        Self {
            name: core::ptr::null(),
            name_len: 0,
            value: core::ptr::null(),
            value_len: 0,
            never_index: false,
            hpack_index: 255,
        }
    }
}

/// wrapper implemented at src/jsc/bindings/c-bindings.cpp
#[repr(C)]
pub struct HPACK {
    self_: *mut c_void,
}

pub struct DecodeResult {
    // TODO(port): lifetime — name/value point into an FFI thread_local shared buffer,
    // valid only until the next decode/encode call. Consider `DecodeResult<'a>`.
    pub name: &'static [u8],
    pub value: &'static [u8],
    pub never_index: bool,
    pub well_know: u16,
    /// offset of the next header position in src
    pub next: usize,
}

#[derive(thiserror::Error, strum::IntoStaticStr, Debug)]
pub enum HpackError {
    #[error("UnableToDecode")]
    UnableToDecode,
    #[error("EmptyHeaderName")]
    EmptyHeaderName,
    #[error("UnableToEncode")]
    UnableToEncode,
}
// TODO(port): impl From<HpackError> for bun_core::Error

impl HPACK {
    pub const LSHPACK_MAX_HEADER_SIZE: usize = 65536;

    pub fn init(max_capacity: u32) -> *mut HPACK {
        // `lshpack_wrapper_init` is `safe fn`: its only precondition is non-null
        // alloc/free callbacks, which the bare (non-Option) fn-ptr types enforce.
        let ptr = lshpack_wrapper_init(
            bun_alloc::mimalloc::mi_malloc,
            bun_alloc::mimalloc::mi_free,
            max_capacity as usize,
        );
        if ptr.is_null() {
            bun_core::out_of_memory();
        }
        ptr
        // TODO(port): wrap in an owning newtype with Drop instead of returning a raw *mut HPACK
    }

    /// DecodeResult name and value uses a thread_local shared buffer and should be copy/cloned before the next decode/encode call
    pub fn decode(&mut self, src: &[u8]) -> Result<DecodeResult, HpackError> {
        let mut header = lshpack_header::default();
        // SAFETY: genuine FFI — only the `(src.as_ptr(), src.len())` pair carries
        // an obligation here (in-bounds read), discharged by `src: &[u8]`. The
        // `self`/`output` preconditions are type-discharged via `&mut` in the
        // extern signature.
        let offset = unsafe { lshpack_wrapper_decode(self, src.as_ptr(), src.len(), &mut header) };
        if offset == 0 {
            return Err(HpackError::UnableToDecode);
        }
        if header.name_len == 0 {
            return Err(HpackError::EmptyHeaderName);
        }

        // SAFETY: lshpack_wrapper_decode writes name/value as offsets into the
        // thread_local `shared_header_buffer` (set via lsxpack_header_prepare_decode),
        // so both pointers are provably non-null after a successful decode. Use bare
        // `from_raw_parts` to avoid a dead null-branch on the per-HTTP/2-header path.
        let (name, value) = unsafe {
            (
                core::slice::from_raw_parts(header.name, header.name_len),
                core::slice::from_raw_parts(header.value, header.value_len),
            )
        };

        Ok(DecodeResult {
            name,
            value,
            next: offset,
            never_index: header.never_index,
            well_know: header.hpack_index,
        })
    }

    /// encode name, value with never_index option into dst_buffer
    /// if name + value length is greater than LSHPACK_MAX_HEADER_SIZE this will return UnableToEncode
    pub fn encode(
        &mut self,
        name: &[u8],
        value: &[u8],
        never_index: bool,
        dst_buffer: &mut [u8],
        dst_buffer_offset: usize,
    ) -> Result<usize, HpackError> {
        // SAFETY: genuine FFI — the three (ptr,len) pairs come straight from
        // live borrowed slices so the C side's reads/writes are in-bounds. The
        // `self` precondition is type-discharged via `&mut` in the extern
        // signature; the ptr+len contracts cannot be encoded over the C ABI.
        let offset = unsafe {
            lshpack_wrapper_encode(
                self,
                name.as_ptr(),
                name.len(),
                value.as_ptr(),
                value.len(),
                never_index as c_int,
                dst_buffer.as_mut_ptr(),
                dst_buffer.len(),
                dst_buffer_offset,
            )
        };
        // PORT NOTE: Zig compared `offset <= 0` on a usize; only `== 0` is reachable.
        if offset == 0 {
            return Err(HpackError::UnableToEncode);
        }
        Ok(offset)
    }

    /// Adjust the encoder's dynamic-table capacity after init. Evicts entries
    /// to fit; the caller is responsible for emitting the RFC 7541 §6.3
    /// Dynamic Table Size Update opcode at the start of the next header block
    /// so the peer's decoder evicts in lockstep.
    pub fn set_encoder_max_capacity(&mut self, max_capacity: u32) {
        lshpack_wrapper_enc_set_max_capacity(self, max_capacity as c_uint);
    }

    /// Raise the decoder's dynamic-table capacity bound to the
    /// SETTINGS_HEADER_TABLE_SIZE value this endpoint advertised in its
    /// connection preface. RFC 7541 §4.2: the peer's encoder may then size
    /// its table up to that value and signal so with a Dynamic Table Size
    /// Update — updates at or below it must decode; updates above it remain
    /// a COMPRESSION_ERROR. Independent of the encoder cap (the peer's
    /// SETTINGS governs that direction).
    pub fn set_decoder_max_capacity(&mut self, max_capacity: u32) {
        lshpack_wrapper_dec_set_max_capacity(self, max_capacity as c_uint);
    }

    // PORT NOTE: Zig `destroy` (raw `*mut HPACK` teardown) is subsumed by the
    // safe [`HpackHandle`] RAII wrapper below — every Rust owner holds an
    // `HpackHandle`, so the raw destructor is private to `HpackHandle::drop`.
}

/// Owning handle for an `HPACK` instance returned by [`HPACK::init`].
///
/// `HPACK::init` allocates via the C wrapper (`lshpack_wrapper_init`, which
/// `mi_malloc`s the struct and `lshpack_{enc,dec}_init`s its internals). The
/// matching teardown is `lshpack_wrapper_deinit`, which runs the lshpack
/// cleanup hooks before freeing — **not** a bare `mi_free`. Wrapping the raw
/// pointer in `Box<HPACK>` (and letting `Box`'s `Drop` free it) therefore
/// leaks the encoder/decoder's internal allocations. Use this handle instead.
pub struct HpackHandle(core::ptr::NonNull<HPACK>);

impl HpackHandle {
    #[inline]
    pub fn new(max_capacity: u32) -> Self {
        // `HPACK::init` already panics (out_of_memory) on null.
        Self(
            core::ptr::NonNull::new(HPACK::init(max_capacity))
                .expect("lshpack_wrapper_init returned null"),
        )
    }
}

impl core::ops::Deref for HpackHandle {
    type Target = HPACK;
    #[inline]
    fn deref(&self) -> &HPACK {
        // SAFETY: `self.0` is the unique live owner of the C allocation.
        unsafe { self.0.as_ref() }
    }
}

impl core::ops::DerefMut for HpackHandle {
    #[inline]
    fn deref_mut(&mut self) -> &mut HPACK {
        // SAFETY: `self.0` is the unique live owner of the C allocation.
        unsafe { self.0.as_mut() }
    }
}

impl Drop for HpackHandle {
    #[inline]
    fn drop(&mut self) {
        // SAFETY: `self.0` came from `lshpack_wrapper_init` (via `HPACK::init`)
        // and `HpackHandle` is its unique owner, so this is the first and only
        // teardown — runs `lshpack_{enc,dec}_cleanup` then frees.
        unsafe { lshpack_wrapper_deinit(self.0.as_ptr()) };
    }
}

// SAFETY: the C wrapper has no thread affinity; lshpack state is not accessed
// concurrently (callers serialize on the owning `H2FrameParser`).
unsafe impl Send for HpackHandle {}

// Non-Option fn pointers: the C ABI repr is identical to `Option<fn>`, but the
// type guarantees non-null, which is the only precondition `lshpack_wrapper_init`
// has — letting it be declared `safe fn` below.
// `alloc` has no caller precondition (mi_malloc is total over usize), so its
// pointer type is safe; `free` retains a caller contract because `mi_free`
// requires "ptr is mimalloc-owned or null" — discharged by the C wrapper,
// not by Rust's type system.
type LshpackWrapperAlloc = extern "C" fn(size: usize) -> *mut c_void;
type LshpackWrapperFree = unsafe extern "C" fn(ptr: *mut c_void);

// TODO(port): move to bun_http_sys
unsafe extern "C" {
    safe fn lshpack_wrapper_init(
        alloc: LshpackWrapperAlloc,
        free: LshpackWrapperFree,
        capacity: usize,
    ) -> *mut HPACK;
    // 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);
    // 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_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);
    // `self_`/`output` are tightened to references (ABI-identical thin ptrs) so
    // those preconditions are type-discharged; the (src,src_len) pair still
    // carries an in-bounds-read contract that the C ABI cannot encode, so the
    // declaration stays unsafe.
    fn lshpack_wrapper_decode(
        self_: &mut HPACK,
        src: *const u8,
        src_len: usize,
        output: &mut lshpack_header,
    ) -> usize;
    // `self_` tightened to a reference; the three (ptr,len) pairs carry
    // in-bounds read/write contracts that the C ABI cannot encode, so the
    // declaration stays unsafe.
    fn lshpack_wrapper_encode(
        self_: &mut HPACK,
        name: *const u8,
        name_len: usize,
        value: *const u8,
        value_len: usize,
        never_index: c_int,
        buffer: *mut u8,
        buffer_len: usize,
        buffer_offset: usize,
    ) -> usize;
}

// ported from: src/http/lshpack.zig