From 910b9dbb42e44a5e489f944242de1d5aaf7ab69a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leo=20Bl=C3=B6cher?= Date: Fri, 1 May 2026 20:26:39 +0100 Subject: [PATCH 1/4] quiche: Add inline variant to ConnectionId The existing `ConnectionId` struct is 24 bytes in size, which is sufficient to store any QUIC v1 connection ID inline. Taking advantage of this allows us to avoid allocations when storing and moving CIDs around throughout quiche and tokio-quiche. The implementation is heavily inspired by `CidOwned` in tokio-quiche, which I will replace in the next commit. This includes optimizations to make hashing and comparisons more efficient by interpreting the entire inline storage as a u64 array to enable fewer comparisons. --- Cargo.toml | 1 + quiche/Cargo.toml | 1 + quiche/src/packet.rs | 246 ++++++++++++++++++++++++++++++++++++++----- 3 files changed, 222 insertions(+), 26 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 067a8e5d62d..e1394bf6af9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ anyhow = { version = "1" } assert_matches = { version = "1" } boring = { version = "4.3" } buffer-pool = { version = "0.2.1", path = "./buffer-pool" } +bytemuck = { version = "1.25.0", features = ["derive", "min_const_generics", "must_cast"] } bytes = { version = "1.11.1" } crossbeam = { version = "0.8.1", default-features = false } dashmap = { version = "6" } diff --git a/quiche/Cargo.toml b/quiche/Cargo.toml index 464aa3668f7..00116e384c5 100644 --- a/quiche/Cargo.toml +++ b/quiche/Cargo.toml @@ -64,6 +64,7 @@ rustdoc-args = ["--cfg", "docsrs"] cdylib-link-lines = { version = "0.1", optional = true } [dependencies] +bytemuck = { workspace = true } bytes = { workspace = true } boring = { workspace = true, optional = true } debug_panic = { workspace = true } diff --git a/quiche/src/packet.rs b/quiche/src/packet.rs index c7437c220f8..9123b7b67de 100644 --- a/quiche/src/packet.rs +++ b/quiche/src/packet.rs @@ -182,38 +182,195 @@ impl Type { } } +/// Special ranged u8 type for [`ConnectionIdInline`]. +/// Restricting the range of values allows the compiler to use free values as +/// niches for enum size optimization. Additionally, range checks during +/// indexing can be optimized away. +#[derive(Clone, Copy, bytemuck::NoUninit)] +#[repr(u8)] +enum CidInlineLen { + Z = 0, + P1 = 1, + P2 = 2, + P3 = 3, + P4 = 4, + P5 = 5, + P6 = 6, + P7 = 7, + P8 = 8, + P9 = 9, + P10 = 10, + P11 = 11, + P12 = 12, + P13 = 13, + P14 = 14, + P15 = 15, + P16 = 16, + P17 = 17, + P18 = 18, + P19 = 19, + P20 = 20, + P21 = 21, + P22 = 22, + P23 = 23, +} + +impl CidInlineLen { + #[inline(always)] + const fn new(len: usize) -> Option { + match len { + 0 => Some(Self::Z), + 1 => Some(Self::P1), + 2 => Some(Self::P2), + 3 => Some(Self::P3), + 4 => Some(Self::P4), + 5 => Some(Self::P5), + 6 => Some(Self::P6), + 7 => Some(Self::P7), + 8 => Some(Self::P8), + 9 => Some(Self::P9), + 10 => Some(Self::P10), + 11 => Some(Self::P11), + 12 => Some(Self::P12), + 13 => Some(Self::P13), + 14 => Some(Self::P14), + 15 => Some(Self::P15), + 16 => Some(Self::P16), + 17 => Some(Self::P17), + 18 => Some(Self::P18), + 19 => Some(Self::P19), + 20 => Some(Self::P20), + 21 => Some(Self::P21), + 22 => Some(Self::P22), + 23 => Some(Self::P23), + _ => None, + } + } + + #[inline(always)] + const fn slice(self, arr: &[u8; 23]) -> &[u8] { + // Can't use `arr.get(..idx)` because indexing is not const yet. + // Due to the limited value range, any range checks will be optimized away + // here. + let idx = self as usize; + arr.split_at(idx).0 + } + + #[inline(always)] + const fn slice_mut(self, arr: &mut [u8; 23]) -> &mut [u8] { + let idx = self as usize; + arr.split_at_mut(idx).0 + } +} + +const _: () = { + // Assert that all values in 0..=23 roundtrip successfully. + // This catches errors in the enum definition & constructor. + let mut v = 0; + while v <= 23 { + let len = CidInlineLen::new(v).unwrap(); + assert!(len as usize == v); + v += 1 + } + + // Assert that values >23 are invalid + assert!(CidInlineLen::new(24).is_none()); + assert!(CidInlineLen::new(0xFF).is_none()); +}; + /// A QUIC connection ID. +#[derive(Clone)] pub struct ConnectionId<'a>(ConnectionIdInner<'a>); +// Assert that ConnectionId maintains the minimum size possible (on x86-64). +// We need at least a variant with `&[u8]` (16 bytes, 1 niche) and a variant +// with a heap allocation (also 16 bytes). Due to both variants being 16 bytes +// in size, with 8-byte alignment, and `&[u8]` only posessing a single niche, +// the enum has to be at least 24 bytes in size. +const _: () = assert!(size_of::() == 24); + +#[derive(Clone)] enum ConnectionIdInner<'a> { - Vec(Vec), + Box(Box<[u8]>), Ref(&'a [u8]), + // Invariant: we use inline storage whenever the CID fits into it. This + // allows faster comparisons and hashing in the common case. + Inline(ConnectionIdInline), +} + +// Use `repr(C)` to control struct layout, and `align(8)` to ensure alignment +// for access as u64 array. +#[derive(Clone, Copy, bytemuck::NoUninit)] +#[repr(C, align(8))] +struct ConnectionIdInline { + storage: [u8; 23], + len: CidInlineLen, +} + +// Assert that regular QUICv1 CIDs fit into ConnectionIdInline. +const _: () = + assert!(ConnectionIdInline::new(&[0; crate::MAX_CONN_ID_LEN]).is_some()); + +impl ConnectionIdInline { + #[inline] + const fn new(cid: &[u8]) -> Option { + let Some(len) = CidInlineLen::new(cid.len()) else { + return None; + }; + + let mut storage = [0; 23]; + len.slice_mut(&mut storage).copy_from_slice(cid); + + Some(Self { storage, len }) + } + + #[inline] + const fn cid_bytes(&self) -> &[u8] { + self.len.slice(&self.storage) + } + + /// Access `self` as an array of u64s for efficient comparisons. + #[inline] + const fn raw_u64(&self) -> &[u64; 3] { + bytemuck::must_cast_ref(self) + } } impl<'a> ConnectionId<'a> { /// Creates a new connection ID from the given vector. #[inline] - pub const fn from_vec(cid: Vec) -> Self { - Self(ConnectionIdInner::Vec(cid)) + pub fn from_vec(cid: Vec) -> Self { + Self(match ConnectionIdInline::new(&cid) { + Some(v) => ConnectionIdInner::Inline(v), + None => ConnectionIdInner::Box(cid.into_boxed_slice()), + }) } /// Creates a new connection ID from the given slice. #[inline] pub const fn from_ref(cid: &'a [u8]) -> Self { - Self(ConnectionIdInner::Ref(cid)) + Self(match ConnectionIdInline::new(cid) { + Some(v) => ConnectionIdInner::Inline(v), + None => ConnectionIdInner::Ref(cid), + }) } /// Returns a new owning connection ID from the given existing one. #[inline] pub fn into_owned(self) -> ConnectionId<'static> { - ConnectionId::from_vec(self.into()) + ConnectionId(match self.0 { + ConnectionIdInner::Box(v) => ConnectionIdInner::Box(v), + // Due to our invariant, we know v does not fit into inline storage + ConnectionIdInner::Ref(v) => ConnectionIdInner::Box(v.into()), + ConnectionIdInner::Inline(v) => ConnectionIdInner::Inline(v), + }) } } impl Default for ConnectionId<'_> { #[inline] fn default() -> Self { - Self::from_vec(Vec::new()) + const { Self::from_ref(&[]) } } } @@ -228,54 +385,91 @@ impl From> for Vec { #[inline] fn from(id: ConnectionId<'_>) -> Self { match id.0 { - ConnectionIdInner::Vec(cid) => cid, - ConnectionIdInner::Ref(cid) => cid.to_vec(), + ConnectionIdInner::Box(cid) => cid.into_vec(), + _ => id.as_ref().to_vec(), } } } -impl PartialEq for ConnectionId<'_> { +impl<'a, 'b> PartialEq> for ConnectionId<'a> { #[inline] - fn eq(&self, other: &Self) -> bool { - self.as_ref() == other.as_ref() + fn eq(&self, other: &ConnectionId<'b>) -> bool { + // If `self` is inline, `other` must be too for their lengths to match + match (&self.0, &other.0) { + (ConnectionIdInner::Inline(us), ConnectionIdInner::Inline(them)) => + us.raw_u64() == them.raw_u64(), + (ConnectionIdInner::Inline(_), _) => false, + (_, ConnectionIdInner::Inline(_)) => false, + _ => self.as_ref() == other.as_ref(), + } } } impl Eq for ConnectionId<'_> {} -impl AsRef<[u8]> for ConnectionId<'_> { +impl std::hash::Hash for ConnectionId<'_> { #[inline] - fn as_ref(&self) -> &[u8] { - match &self.0 { - ConnectionIdInner::Vec(v) => v.as_ref(), - ConnectionIdInner::Ref(v) => v, + fn hash(&self, state: &mut H) { + match self.0 { + // No prefix collision: Inline and non-inline variants pass different + // length prefixes + ConnectionIdInner::Inline(v) => v.raw_u64().hash(state), + _ => self.as_ref().hash(state), } } } -impl std::hash::Hash for ConnectionId<'_> { +impl Ord for ConnectionId<'_> { #[inline] - fn hash(&self, state: &mut H) { - self.as_ref().hash(state); + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + use std::cmp::Ordering; + + /// Sort slices by length first, then lexicographically. + #[inline] + fn cmp_length_then_lex(a: &[u8], b: &[u8]) -> Ordering { + match a.len().cmp(&b.len()) { + Ordering::Equal => a.cmp(b), + res => res, + } + } + + // We _should_ be able to compare the AsRef slices directly, but the + // explicit match here generates better assembly + match (&self.0, &other.0) { + (ConnectionIdInner::Inline(us), ConnectionIdInner::Inline(them)) => + cmp_length_then_lex(us.cid_bytes(), them.cid_bytes()), + // Inline's length is always less than the other variants' lengths + (ConnectionIdInner::Inline(_), _) => Ordering::Less, + (_, ConnectionIdInner::Inline(_)) => Ordering::Greater, + _ => cmp_length_then_lex(self.as_ref(), other.as_ref()), + } } } -impl std::ops::Deref for ConnectionId<'_> { - type Target = [u8]; +impl PartialOrd for ConnectionId<'_> { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl AsRef<[u8]> for ConnectionId<'_> { #[inline] - fn deref(&self) -> &[u8] { + fn as_ref(&self) -> &[u8] { match &self.0 { - ConnectionIdInner::Vec(v) => v.as_ref(), + ConnectionIdInner::Box(v) => v, ConnectionIdInner::Ref(v) => v, + ConnectionIdInner::Inline(v) => v.cid_bytes(), } } } -impl Clone for ConnectionId<'_> { +impl std::ops::Deref for ConnectionId<'_> { + type Target = [u8]; + #[inline] - fn clone(&self) -> Self { - Self::from_vec(self.as_ref().to_vec()) + fn deref(&self) -> &[u8] { + self.as_ref() } } From 4b9c75fa97358de30a2946926fc1b2bcb32b299c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leo=20Bl=C3=B6cher?= Date: Fri, 1 May 2026 23:38:03 +0100 Subject: [PATCH 2/4] tokio-quiche: Replace CidOwned with quiche::ConnectionId --- tokio-quiche/src/quic/connection/map.rs | 106 ++++-------------------- tokio-quiche/src/quic/router/mod.rs | 6 +- 2 files changed, 21 insertions(+), 91 deletions(-) diff --git a/tokio-quiche/src/quic/connection/map.rs b/tokio-quiche/src/quic/connection/map.rs index 30cada1d627..d4ddb1195ba 100644 --- a/tokio-quiche/src/quic/connection/map.rs +++ b/tokio-quiche/src/quic/connection/map.rs @@ -34,64 +34,15 @@ use quiche::MAX_CONN_ID_LEN; use std::collections::BTreeMap; use tokio::sync::mpsc; -const U64_SZ: usize = std::mem::size_of::(); -const MAX_CONN_ID_QUADS: usize = MAX_CONN_ID_LEN.div_ceil(U64_SZ); -const CONN_ID_USABLE_LEN: usize = min_usize( - // Last byte in CidOwned::Optimized stores CID length - MAX_CONN_ID_QUADS * U64_SZ - 1, - // CID length must fit in 1 byte - min_usize(MAX_CONN_ID_LEN, u8::MAX as _), -); - -const fn min_usize(v1: usize, v2: usize) -> usize { - if v1 < v2 { - v1 - } else { - v2 - } -} - -/// A non unique connection identifier, multiple Cids can map to the same -/// conenction. +/// Newtype wrapper for `ConnectionId<'static>` to allow `Borrow` with a +/// different lifetime. This impl would conflict with `impl Borrow for T` +/// directly on `ConnectionId`. #[derive(PartialEq, Eq, PartialOrd, Ord)] -enum CidOwned { - /// The QUIC connections IDs theoretically have unbounded length, so for the - /// generic case a boxed slice is used to store the ID. - Generic(Box<[u8]>), - /// For QUIC version 1 (the one that actually exists) the maximal ID size is - /// `20`, which should correspond to the `MAX_CONN_ID_LEN` value. For - /// this common case, we store the ID in a u64 array for faster - /// comparison (and therefore BTreeMap lookups). - Optimized([u64; MAX_CONN_ID_QUADS]), -} - -impl From<&ConnectionId<'_>> for CidOwned { - #[inline(always)] - fn from(value: &ConnectionId<'_>) -> Self { - if value.len() > CONN_ID_USABLE_LEN { - return CidOwned::Generic(value.as_ref().into()); - } - - let mut cid = [0; MAX_CONN_ID_QUADS]; +struct CidOwned(ConnectionId<'static>); - value - .chunks(U64_SZ) - .map(|c| match c.try_into() { - Ok(v) => u64::from_le_bytes(v), - Err(_) => { - let mut remainder = [0u8; U64_SZ]; - remainder[..c.len()].copy_from_slice(c); - u64::from_le_bytes(remainder) - }, - }) - .enumerate() - .for_each(|(i, v)| cid[i] = v); - - // In order to differentiate cids with zeroes as opposed to shorter cids, - // append the cid length. - *cid.last_mut().unwrap() |= (value.len() as u64) << 56; - - CidOwned::Optimized(cid) +impl<'a> std::borrow::Borrow> for CidOwned { + fn borrow(&self) -> &ConnectionId<'a> { + &self.0 } } @@ -107,25 +58,27 @@ pub(crate) struct ConnectionMap { impl ConnectionMap { pub(crate) fn insert( - &mut self, cid: &ConnectionId<'_>, conn: &InitialQuicConnection, + &mut self, cid: ConnectionId<'static>, + conn: &InitialQuicConnection, ) where Tx: DatagramSocketSend + Send + 'static, M: Metrics, { let ev_sender = conn.incoming_ev_sender.clone(); - self.quic_id_map.insert(cid.into(), ev_sender); + self.quic_id_map.insert(CidOwned(cid), ev_sender); } pub(crate) fn map_cid( - &mut self, existing_cid: &ConnectionId<'_>, new_cid: &ConnectionId<'_>, + &mut self, existing_cid: &ConnectionId, new_cid: ConnectionId<'static>, ) { - if let Some(ev_sender) = self.quic_id_map.get(&existing_cid.into()) { - self.quic_id_map.insert(new_cid.into(), ev_sender.clone()); + if let Some(ev_sender) = self.quic_id_map.get(existing_cid) { + self.quic_id_map + .insert(CidOwned(new_cid), ev_sender.clone()); } } - pub(crate) fn unmap_cid(&mut self, cid: &ConnectionId<'_>) { - self.quic_id_map.remove(&cid.into()); + pub(crate) fn unmap_cid(&mut self, cid: &ConnectionId) { + self.quic_id_map.remove(cid); } pub(crate) fn get( @@ -135,32 +88,9 @@ impl ConnectionMap { // Although both branches run the same code, the one here will // generate an optimized version for the length we are // using, as opposed to temporary cids sent by clients. - self.quic_id_map.get(&id.into()) + self.quic_id_map.get(id) } else { - self.quic_id_map.get(&id.into()) + self.quic_id_map.get(id) } } } - -#[cfg(test)] -mod tests { - use super::*; - use quiche::ConnectionId; - - #[test] - fn cid_storage() { - let max_v1_cid = ConnectionId::from_ref(&[0xfa; MAX_CONN_ID_LEN]); - let optimized = CidOwned::from(&max_v1_cid); - assert!( - matches!(optimized, CidOwned::Optimized(_)), - "QUIC v1 CID is not stored inline" - ); - - let oversize_cid = ConnectionId::from_ref(&[0x1b; MAX_CONN_ID_LEN + 20]); - let boxed = CidOwned::from(&oversize_cid); - assert!( - matches!(boxed, CidOwned::Generic(_)), - "Oversized CID is not boxed" - ); - } -} diff --git a/tokio-quiche/src/quic/router/mod.rs b/tokio-quiche/src/quic/router/mod.rs index e66a0bcbb46..8982bb9ff0b 100644 --- a/tokio-quiche/src/quic/router/mod.rs +++ b/tokio-quiche/src/quic/router/mod.rs @@ -354,13 +354,13 @@ where handshake_start_time, )); - self.conns.insert(&scid, &conn); + self.conns.insert(scid.clone(), &conn); // Add the client-generated "pending" connection ID to the map as well. // This is only required for QUIC servers, because clients can send // Initial packets with arbitrary DCIDs to servers. if let Some(pending_cid) = pending_cid { - self.conns.map_cid(&scid, &pending_cid); + self.conns.map_cid(&scid, pending_cid); } self.metrics.accepted_initial_packet_count().inc(); @@ -632,7 +632,7 @@ where ConnectionMapCommand::MapCid { existing_cid, new_cid, - } => self.conns.map_cid(&existing_cid, &new_cid), + } => self.conns.map_cid(&existing_cid, new_cid), ConnectionMapCommand::UnmapCid(cid) => self.conns.unmap_cid(&cid), } } From 4a1b9510f28d8fd0af514fd18b7be3646481d8e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leo=20Bl=C3=B6cher?= Date: Fri, 1 May 2026 23:03:51 +0100 Subject: [PATCH 3/4] quiche: Avoid roundtripping ConnectionId through Vec Now that ConnectionId has inline storage, `cid.to_vec().into()` is a needlessly inefficient way to clone it. --- quiche/src/cid.rs | 3 +-- quiche/src/lib.rs | 37 ++++++++++++++----------------------- quiche/src/test_utils.rs | 8 ++------ 3 files changed, 17 insertions(+), 31 deletions(-) diff --git a/quiche/src/cid.rs b/quiche/src/cid.rs index 685b5964a0a..065efd0c44e 100644 --- a/quiche/src/cid.rs +++ b/quiche/src/cid.rs @@ -258,8 +258,7 @@ impl ConnectionIdentifiers { // Record the zero-length SCID status. let zero_length_scid = initial_scid.is_empty(); - let initial_scid = - ConnectionId::from_ref(initial_scid.as_ref()).into_owned(); + let initial_scid = initial_scid.clone().into_owned(); // We need to track up to (2 * source_conn_id_limit - 1) source // Connection IDs when the host wants to force their renewal. diff --git a/quiche/src/lib.rs b/quiche/src/lib.rs index c13ec029197..29b2cdc958c 100644 --- a/quiche/src/lib.rs +++ b/quiche/src/lib.rs @@ -2008,7 +2008,7 @@ impl Connection { if let Some(client_dcid) = client_dcid { // The Minimum length is 8. // See https://datatracker.ietf.org/doc/html/rfc9000#section-7.2-3 - if client_dcid.to_vec().len() < 8 { + if client_dcid.len() < 8 { return Err(Error::InvalidDcidInitialization); } } @@ -2247,16 +2247,16 @@ impl Connection { if let Some(retry_cids) = retry_cids { conn.local_transport_params .original_destination_connection_id = - Some(retry_cids.original_destination_cid.to_vec().into()); + Some(retry_cids.original_destination_cid.clone().into_owned()); conn.local_transport_params.retry_source_connection_id = - Some(retry_cids.retry_source_cid.to_vec().into()); + Some(retry_cids.retry_source_cid.clone().into_owned()); conn.did_retry = true; } conn.local_transport_params.initial_source_connection_id = - Some(conn.ids.get_scid(0)?.cid.to_vec().into()); + Some(conn.ids.get_scid(0)?.cid.clone()); conn.handshake.init(is_server)?; @@ -2286,11 +2286,7 @@ impl Connection { )?; let reset_token = conn.peer_transport_params.stateless_reset_token; - conn.set_initial_dcid( - dcid.to_vec().into(), - reset_token, - active_path_id, - )?; + conn.set_initial_dcid(dcid.into(), reset_token, active_path_id)?; conn.crypto_ctx[packet::Epoch::Initial].crypto_open = Some(aead_open); conn.crypto_ctx[packet::Epoch::Initial].crypto_seal = Some(aead_seal); @@ -3369,8 +3365,7 @@ impl Connection { // Now that we decrypted the packet, let's see if we can map it to an // existing path. let recv_pid = if hdr.ty == Type::Short && self.got_peer_conn_id { - let pkt_dcid = ConnectionId::from_ref(&hdr.dcid); - self.get_or_create_recv_path_id(recv_pid, &pkt_dcid, buf_len, info)? + self.get_or_create_recv_path_id(recv_pid, &hdr.dcid, buf_len, info)? } else { // During handshake, we are on the initial path. self.paths.get_active_path_id()? @@ -3454,8 +3449,7 @@ impl Connection { if !self.did_retry { self.local_transport_params - .original_destination_connection_id = - Some(hdr.dcid.to_vec().into()); + .original_destination_connection_id = Some(hdr.dcid.clone()); self.encode_transport_params()?; } @@ -4426,11 +4420,10 @@ impl Connection { let dcid_seq = path.active_dcid_seq.ok_or(Error::OutOfIdentifiers)?; - let dcid = - ConnectionId::from_ref(self.ids.get_dcid(dcid_seq)?.cid.as_ref()); + let dcid = self.ids.get_dcid(dcid_seq)?.cid.clone(); let scid = if let Some(scid_seq) = path.active_scid_seq { - ConnectionId::from_ref(self.ids.get_scid(scid_seq)?.cid.as_ref()) + self.ids.get_scid(scid_seq)?.cid.clone() } else if pkt_type == Type::Short { ConnectionId::default() } else { @@ -7286,7 +7279,7 @@ impl Connection { &mut self, scid: &ConnectionId, reset_token: u128, retire_if_needed: bool, ) -> Result { self.ids.new_scid( - scid.to_vec().into(), + scid.clone().into_owned(), Some(reset_token), true, None, @@ -7605,13 +7598,12 @@ impl Connection { if let Ok(path) = self.paths.get_active() { if let Some(active_scid_seq) = path.active_scid_seq { if let Ok(e) = self.ids.get_scid(active_scid_seq) { - return ConnectionId::from_ref(e.cid.as_ref()); + return e.cid.clone(); } } } - let e = self.ids.oldest_scid(); - ConnectionId::from_ref(e.cid.as_ref()) + self.ids.oldest_scid().cid.clone() } /// Returns all active source connection IDs. @@ -7632,13 +7624,12 @@ impl Connection { if let Ok(path) = self.paths.get_active() { if let Some(active_dcid_seq) = path.active_dcid_seq { if let Ok(e) = self.ids.get_dcid(active_dcid_seq) { - return ConnectionId::from_ref(e.cid.as_ref()); + return e.cid.clone(); } } } - let e = self.ids.oldest_dcid(); - ConnectionId::from_ref(e.cid.as_ref()) + self.ids.oldest_dcid().cid.clone() } /// Returns the PMTU for the active path if it exists. diff --git a/quiche/src/test_utils.rs b/quiche/src/test_utils.rs index 2f403e699fc..5a193e789d9 100644 --- a/quiche/src/test_utils.rs +++ b/quiche/src/test_utils.rs @@ -502,12 +502,8 @@ pub fn encode_pkt( let hdr = Header { ty: pkt_type, version: conn.version, - dcid: ConnectionId::from_ref( - conn.ids.get_dcid(*active_dcid_seq)?.cid.as_ref(), - ), - scid: ConnectionId::from_ref( - conn.ids.get_scid(*active_scid_seq)?.cid.as_ref(), - ), + dcid: conn.ids.get_dcid(*active_dcid_seq)?.cid.clone(), + scid: conn.ids.get_scid(*active_scid_seq)?.cid.clone(), pkt_num: pn, pkt_num_len: pn_len, token: conn.token.clone(), From 0119ddfd9cb7402efa5bcf638d13b985018154c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leo=20Bl=C3=B6cher?= Date: Tue, 5 May 2026 14:18:03 +0100 Subject: [PATCH 4/4] chore: Bump MSRV to 1.87 & fix lints `ConnectionIdInline` needs Rust 1.87 for const `slice::split_at_mut_checked` in its `new()` method. --- AGENTS.md | 2 +- Dockerfile | 2 +- README.md | 2 +- datagram-socket/src/buffer.rs | 2 +- h3i/src/client/mod.rs | 2 +- quiche/Cargo.toml | 2 +- quiche/src/h3/mod.rs | 12 ++++++------ tokio-quiche/Cargo.toml | 2 +- tokio-quiche/src/metrics/labels.rs | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d4fcf53f971..3e741596b80 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -128,7 +128,7 @@ make docker-build # quiche-base + quiche-qns ## NOTES - **No git submodules**: BoringSSL is built by `boring-sys`; `cmake` must be available. -- **MSRV 1.85**: `rust-version` field in Cargo.toml. +- **MSRV 1.87**: `rust-version` field in Cargo.toml. - **Doc tests are separate**: `cargo test --all-targets` does NOT run doc tests (cargo#6669). - **`BORING_BSSL_PATH`**: env var to point `boring-sys` at a custom BoringSSL source tree. - **`RUSTFLAGS="-D warnings"`**: CI enforces; all warnings are errors. diff --git a/Dockerfile b/Dockerfile index 50f08883939..1214c7f5c44 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.86 AS build +FROM rust:1.87 AS build WORKDIR /build diff --git a/README.md b/README.md index ff2be4eb3a2..14e056caa3f 100644 --- a/README.md +++ b/README.md @@ -305,7 +305,7 @@ is disabled by default), by passing ``--features ffi`` to ``cargo``. Building -------- -quiche requires Rust 1.85 or later to build. The latest stable Rust release can +quiche requires Rust 1.87 or later to build. The latest stable Rust release can be installed using [rustup](https://rustup.rs/). Once the Rust build environment is setup, the quiche source code can be fetched diff --git a/datagram-socket/src/buffer.rs b/datagram-socket/src/buffer.rs index d3133b89900..8fcdd7b42db 100644 --- a/datagram-socket/src/buffer.rs +++ b/datagram-socket/src/buffer.rs @@ -191,7 +191,7 @@ impl std::fmt::Debug for DgramBuffer { // Render payload bytes as two-char hex, underscore-separated after every // 4. for (i, byte) in self.as_slice().iter().enumerate() { - if i > 0 && i % 4 == 0 { + if i > 0 && i.is_multiple_of(4) { f.write_str("_")?; } write!(f, "{:02x}", byte)?; diff --git a/h3i/src/client/mod.rs b/h3i/src/client/mod.rs index 71d770c5e02..7e732d53280 100644 --- a/h3i/src/client/mod.rs +++ b/h3i/src/client/mod.rs @@ -374,7 +374,7 @@ pub(crate) fn parse_streams( for stream in conn.readable() { // TODO: ignoring control streams - if stream % 4 != 0 { + if !stream.is_multiple_of(4) { continue; } diff --git a/quiche/Cargo.toml b/quiche/Cargo.toml index 00116e384c5..4d2d1ea18fb 100644 --- a/quiche/Cargo.toml +++ b/quiche/Cargo.toml @@ -19,7 +19,7 @@ include = [ "/quiche.svg", "/src", ] -rust-version = "1.85" +rust-version = "1.87" [features] default = ["boringssl-boring-crate"] diff --git a/quiche/src/h3/mod.rs b/quiche/src/h3/mod.rs index 94133715ec5..ff4acf024a3 100644 --- a/quiche/src/h3/mod.rs +++ b/quiche/src/h3/mod.rs @@ -1639,7 +1639,7 @@ impl Connection { let len = body.as_ref().len(); // Validate that it is sane to send data on the stream. - if stream_id % 4 != 0 { + if !stream_id.is_multiple_of(4) { return Err(Error::FrameUnexpected); } @@ -1917,7 +1917,7 @@ impl Connection { return Err(Error::FrameUnexpected); } - if stream_id % 4 != 0 { + if !stream_id.is_multiple_of(4) { return Err(Error::FrameUnexpected); } @@ -2170,7 +2170,7 @@ impl Connection { id = 0; } - if self.is_server && id % 4 != 0 { + if self.is_server && !id.is_multiple_of(4) { return Err(Error::IdError); } @@ -3039,7 +3039,7 @@ impl Connection { }, frame::Frame::GoAway { id } => { - if !self.is_server && id % 4 != 0 { + if !self.is_server && !id.is_multiple_of(4) { conn.close( true, Error::FrameUnexpected.to_wire(), @@ -3101,7 +3101,7 @@ impl Connection { return Err(Error::FrameUnexpected); } - if stream_id % 4 != 0 { + if !stream_id.is_multiple_of(4) { conn.close( true, Error::FrameUnexpected.to_wire(), @@ -3132,7 +3132,7 @@ impl Connection { return Err(Error::FrameUnexpected); } - if prioritized_element_id % 4 != 0 { + if !prioritized_element_id.is_multiple_of(4) { conn.close( true, Error::FrameUnexpected.to_wire(), diff --git a/tokio-quiche/Cargo.toml b/tokio-quiche/Cargo.toml index 2ef072de7a8..ce85f90d7bc 100644 --- a/tokio-quiche/Cargo.toml +++ b/tokio-quiche/Cargo.toml @@ -8,7 +8,7 @@ license = { workspace = true } keywords = ["quic", "http3", "tokio"] categories = { workspace = true } readme = "README.md" -rust-version = "1.85" +rust-version = "1.87" [features] default = ["qlog-gzip", "qlog-zstd"] diff --git a/tokio-quiche/src/metrics/labels.rs b/tokio-quiche/src/metrics/labels.rs index 14d5193a5eb..04cb2bddb29 100644 --- a/tokio-quiche/src/metrics/labels.rs +++ b/tokio-quiche/src/metrics/labels.rs @@ -158,7 +158,7 @@ impl Serialize for H3Error { let code = self.0; // https://www.iana.org/assignments/http3-parameters/http3-parameters.xhtml - let v = if code > 0x21 && (code - 0x21) % 0x1f == 0 { + let v = if code > 0x21 && (code - 0x21).is_multiple_of(0x1f) { "H3_GREASE" } else { match code {