use bun_collections::StringHashMap;
use bun_core::{strings, time::timestamp};
use crate::h3_client::h3_client;
#[derive(Copy, Clone)]
pub struct Entry {
pub port: u16,
pub ma: u32,
}
impl Default for Entry {
fn default() -> Self {
Self { port: 0, ma: 86400 }
}
}
#[derive(thiserror::Error, strum::IntoStaticStr, Debug)]
pub enum ParseError {
#[error("Clear")]
Clear,
}
impl From<ParseError> for bun_core::Error {
fn from(_: ParseError) -> Self {
bun_core::err!("Clear")
}
}
pub fn parse(field_value: &[u8]) -> Result<Option<Entry>, ParseError> {
let value = strings::trim(field_value, b" \t");
if value.is_empty() {
return Ok(None);
}
if strings::eql_case_insensitive_ascii(value, b"clear", true) {
return Err(ParseError::Clear);
}
for raw_entry in value.split(|b| *b == b',') {
let entry = strings::trim(raw_entry, b" \t");
if entry.is_empty() {
continue;
}
let mut params = entry.split(|b| *b == b';');
let alternative = strings::trim(params.next().unwrap(), b" \t");
let Some(eq) = strings::index_of_char(alternative, b'=') else {
continue;
};
let eq = eq as usize;
let proto = &alternative[..eq];
if !strings::eql_case_insensitive_ascii(proto, b"h3", true) {
continue;
}
let mut auth = strings::trim(&alternative[eq + 1..], b" \t");
if auth.len() >= 2 && auth[0] == b'"' && auth[auth.len() - 1] == b'"' {
auth = &auth[1..auth.len() - 1];
}
let Some(colon) = auth.iter().rposition(|&b| b == b':') else {
continue;
};
if colon != 0 {
continue;
}
let Some(port) = strings::parse_int::<u16>(&auth[colon + 1..], 10).ok() else {
continue;
};
if port == 0 {
continue;
}
let mut result = Entry {
port,
..Entry::default()
};
for raw_param in params {
let param = strings::trim(raw_param, b" \t");
let Some(peq) = strings::index_of_char(param, b'=') else {
continue;
};
let peq = peq as usize;
if strings::eql_case_insensitive_ascii(¶m[..peq], b"ma", true) {
result.ma = strings::parse_int::<u32>(¶m[peq + 1..], 10).unwrap_or(result.ma);
}
}
return Ok(Some(result));
}
Ok(None)
}
#[derive(Copy, Clone)]
struct Record {
h3_port: u16,
expires_at: i64,
}
static CACHE: bun_core::RacyCell<Option<StringHashMap<Record>>> = bun_core::RacyCell::new(None);
fn cache() -> &'static mut StringHashMap<Record> {
unsafe { (*CACHE.get()).get_or_insert_with(StringHashMap::default) }
}
const MAX_ENTRIES: usize = 256;
fn key<'a>(buf: &'a mut [u8], hostname: &[u8], port: u16) -> &'a [u8] {
use std::io::Write;
let mut cursor: &mut [u8] = buf;
cursor.write_all(hostname).expect("unreachable");
write!(cursor, ":{}", port).expect("unreachable");
let remaining = cursor.len();
let written = buf.len() - remaining;
&buf[..written]
}
fn sweep_expired(now: i64) {
let cache = cache();
'outer: loop {
let mut to_remove: Option<Box<[u8]>> = None;
for (k, v) in cache.iter() {
if now >= v.expires_at {
to_remove = Some(Box::<[u8]>::from(&**k));
break;
}
}
match to_remove {
Some(k) => {
cache.remove(&k[..]);
}
None => break 'outer,
}
}
}
pub(crate) fn record(origin_host: &[u8], origin_port: u16, field_value: &[u8]) {
let mut buf = [0u8; 256 + 8];
if origin_host.len() > 256 {
return;
}
let k = key(&mut buf, origin_host, origin_port);
let entry = match parse(field_value) {
Err(ParseError::Clear) => {
cache().remove(k);
bun_core::scoped_log!(h3_client, "alt-svc clear {}", bstr::BStr::new(k));
return;
}
Ok(None) => return,
Ok(Some(e)) => e,
};
let now = timestamp();
if cache().len() >= MAX_ENTRIES && !cache().contains_key(k) {
sweep_expired(now);
if cache().len() >= MAX_ENTRIES {
return;
}
}
let _ = cache().put(
k,
Record {
h3_port: entry.port,
expires_at: now + i64::from(entry.ma),
},
);
bun_core::scoped_log!(
h3_client,
"alt-svc h3 {} -> :{} ma={}",
bstr::BStr::new(k),
entry.port,
entry.ma
);
}
pub(crate) fn lookup(origin_host: &[u8], origin_port: u16) -> Option<u16> {
let mut buf = [0u8; 256 + 8];
if origin_host.len() > 256 {
return None;
}
let k = key(&mut buf, origin_host, origin_port);
let rec = *cache().get(k)?;
if timestamp() >= rec.expires_at {
cache().remove(k);
return None;
}
Some(rec.h3_port)
}