// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// A transport parameter's identifier (RFC 9000 §18.2, and RFC 9221's datagram
/// extension). `Other` carries anything else: the block is read and written generically,
/// so a parameter this build has no name for survives a round trip intact.
pub(all) enum Kind {
OriginalDcid
MaxIdleTimeout
StatelessResetToken
MaxUdpPayload
MaxData
MaxStreamDataBidiLocal
MaxStreamDataBidiRemote
MaxStreamDataUni
MaxStreamsBidi
MaxStreamsUni
AckDelayExponent
MaxAckDelay
DisableActiveMigration
PreferredAddress
ActiveConnectionIdLimit
InitialScid
RetryScid
MaxDatagramFrameSize
Other(UInt64)
} derive(Eq, Debug)
///|
/// The identifier on the wire.
pub fn Kind::code(self : Kind) -> UInt64 {
match self {
OriginalDcid => 0x00
MaxIdleTimeout => 0x01
StatelessResetToken => 0x02
MaxUdpPayload => 0x03
MaxData => 0x04
MaxStreamDataBidiLocal => 0x05
MaxStreamDataBidiRemote => 0x06
MaxStreamDataUni => 0x07
MaxStreamsBidi => 0x08
MaxStreamsUni => 0x09
AckDelayExponent => 0x0a
MaxAckDelay => 0x0b
DisableActiveMigration => 0x0c
PreferredAddress => 0x0d
ActiveConnectionIdLimit => 0x0e
InitialScid => 0x0f
RetryScid => 0x10
MaxDatagramFrameSize => 0x20
Other(code) => code
}
}
///|
/// The parameter a wire identifier names.
pub fn Kind::of(code : UInt64) -> Kind {
match code {
0x00 => OriginalDcid
0x01 => MaxIdleTimeout
0x02 => StatelessResetToken
0x03 => MaxUdpPayload
0x04 => MaxData
0x05 => MaxStreamDataBidiLocal
0x06 => MaxStreamDataBidiRemote
0x07 => MaxStreamDataUni
0x08 => MaxStreamsBidi
0x09 => MaxStreamsUni
0x0a => AckDelayExponent
0x0b => MaxAckDelay
0x0c => DisableActiveMigration
0x0d => PreferredAddress
0x0e => ActiveConnectionIdLimit
0x0f => InitialScid
0x10 => RetryScid
0x20 => MaxDatagramFrameSize
_ => Other(code)
}
}
///|
/// Whether only a server may send this parameter (RFC 9000 §18.2's closing paragraph).
/// A client that sends one is a TRANSPORT_PARAMETER_ERROR.
pub fn Kind::server_only(self : Kind) -> Bool {
match self {
OriginalDcid | PreferredAddress | RetryScid | StatelessResetToken => true
_ => false
}
}
///|
/// One transport parameter: what it is, and its value bytes. An integer parameter's
/// value is a single varint — `Param::int` writes one and `Param::as_int` reads one.
pub(all) struct Param {
kind : Kind
value : Bytes
} derive(Eq, Debug)
///|
/// A parameter whose value is one varint (RFC 9000 §18.2).
pub fn Param::int(kind : Kind, value : UInt64) -> Param {
{ kind, value: @quic.encode(value), }
}
///|
/// A parameter with no value, which is how the flag parameters are carried.
pub fn Param::flag(kind : Kind) -> Param {
{ kind, value: b"", }
}
///|
/// The integer a varint-valued parameter carries, or `None` when its value is not
/// exactly one well-formed varint.
pub fn Param::as_int(self : Param) -> UInt64? {
guard @quic.decode(self.value[:]) is Some((v, n)) else { return None }
if n == self.value.length() {
Some(v)
} else {
None
}
}
///|
/// Encode a block: each parameter as its identifier varint, a length varint, then its
/// value (RFC 9000 §18).
pub fn encode(params : ArrayView[Param]) -> Bytes {
let buf = Buffer()
for p in params {
buf.write_bytes(@quic.encode(p.kind.code())[:])
buf.write_bytes(@quic.encode(p.value.length().to_uint64())[:])
buf.write_bytes(p.value[:])
}
buf.to_bytes()
}
///|
/// Decode a block, keeping the order and any duplicates so the caller can apply
/// RFC 9000 §7.4's rules itself. `None` on a truncated parameter.
pub fn decode(b : BytesView) -> Array[Param]? {
let out : Array[Param] = []
let mut at = 0
while at < b.length() {
guard @quic.decode(b[at:]) is Some((code, used)) else { return None }
at = at + used
guard @quic.decode(b[at:]) is Some((length, used)) else { return None }
at = at + used
let n = length.to_int()
if n < 0 || b.length() < at + n {
return None
}
out.push({ kind: Kind::of(code), value: b[at:at + n].to_owned(), })
at = at + n
}
Some(out)
}
///|
/// The parameter in a block, or `None`. A repeated parameter is a connection error, so
/// the first one is what counts.
pub fn find(params : ArrayView[Param], kind : Kind) -> Param? {
for p in params {
if p.kind == kind {
return Some(p)
}
}
None
}
///|
/// What a block says, with RFC 9000 §18.2's default for everything it leaves out.
///
/// The integer parameters only; the connection IDs, the preferred address and the
/// stateless reset token stay in the block, because they are identifiers and addresses
/// rather than limits and the connection reads them where it needs them.
pub(all) struct Limits {
idle_timeout : UInt64
udp_payload : UInt64
data : UInt64
stream_data_bidi_local : UInt64
stream_data_bidi_remote : UInt64
stream_data_uni : UInt64
streams_bidi : UInt64
streams_uni : UInt64
ack_delay_exponent : UInt64
max_ack_delay : UInt64
active_connection_ids : UInt64
migration : Bool
datagram_frame : UInt64
} derive(Eq, Debug)
///|
/// RFC 9000 §18.2's defaults: no idle timeout, the largest UDP payload the protocol
/// permits, no flow-control credit and no streams until the peer says otherwise, an
/// acknowledgement delay exponent of three, twenty-five milliseconds of acknowledgement
/// delay, two connection IDs, migration allowed, and no datagram frames.
pub let limits : Limits = {
idle_timeout: 0,
udp_payload: 65527,
data: 0,
stream_data_bidi_local: 0,
stream_data_bidi_remote: 0,
stream_data_uni: 0,
streams_bidi: 0,
streams_uni: 0,
ack_delay_exponent: 3,
max_ack_delay: 25,
active_connection_ids: 2,
migration: true,
datagram_frame: 0,
}
///|
/// Limits by name, each defaulting to RFC 9000 §18.2's.
pub fn Limits::new(
idle_timeout? : UInt64 = 0,
udp_payload? : UInt64 = 65527,
data? : UInt64 = 0,
stream_data_bidi_local? : UInt64 = 0,
stream_data_bidi_remote? : UInt64 = 0,
stream_data_uni? : UInt64 = 0,
streams_bidi? : UInt64 = 0,
streams_uni? : UInt64 = 0,
ack_delay_exponent? : UInt64 = 3,
max_ack_delay? : UInt64 = 25,
active_connection_ids? : UInt64 = 2,
migration? : Bool = true,
datagram_frame? : UInt64 = 0,
) -> Limits {
{
idle_timeout,
udp_payload,
data,
stream_data_bidi_local,
stream_data_bidi_remote,
stream_data_uni,
streams_bidi,
streams_uni,
ack_delay_exponent,
max_ack_delay,
active_connection_ids,
migration,
datagram_frame,
}
}
///|
/// The limits a block conveys, starting from `base` and applying what the block sets.
///
/// A parameter whose value is not a well-formed varint is left at its default rather
/// than refused: §18.1 makes that a connection error, and deciding to close is the
/// connection's call, not this reader's.
pub fn Limits::read(
params : ArrayView[Param],
base? : Limits = limits,
) -> Limits {
let mut out = base
for p in params {
let n = p.as_int()
match (p.kind, n) {
(MaxIdleTimeout, Some(v)) => out = { ..out, idle_timeout: v, }
(MaxUdpPayload, Some(v)) => out = { ..out, udp_payload: v, }
(MaxData, Some(v)) => out = { ..out, data: v, }
(MaxStreamDataBidiLocal, Some(v)) =>
out = { ..out, stream_data_bidi_local: v, }
(MaxStreamDataBidiRemote, Some(v)) =>
out = { ..out, stream_data_bidi_remote: v, }
(MaxStreamDataUni, Some(v)) => out = { ..out, stream_data_uni: v, }
(MaxStreamsBidi, Some(v)) => out = { ..out, streams_bidi: v, }
(MaxStreamsUni, Some(v)) => out = { ..out, streams_uni: v, }
(AckDelayExponent, Some(v)) => out = { ..out, ack_delay_exponent: v, }
(MaxAckDelay, Some(v)) => out = { ..out, max_ack_delay: v, }
(ActiveConnectionIdLimit, Some(v)) =>
out = { ..out, active_connection_ids: v, }
(MaxDatagramFrameSize, Some(v)) => out = { ..out, datagram_frame: v, }
(DisableActiveMigration, _) => out = { ..out, migration: false, }
_ => ()
}
}
out
}
///|
/// The block these limits amount to, leaving out everything still at its default so a
/// handshake carries only what it changes.
pub fn Limits::params(self : Limits) -> Array[Param] {
let out : Array[Param] = []
let put = (kind : Kind, v : UInt64, base : UInt64) => {
if v != base {
out.push(Param::int(kind, v))
}
}
put(MaxIdleTimeout, self.idle_timeout, limits.idle_timeout)
put(MaxUdpPayload, self.udp_payload, limits.udp_payload)
put(MaxData, self.data, limits.data)
put(
MaxStreamDataBidiLocal,
self.stream_data_bidi_local,
limits.stream_data_bidi_local,
)
put(
MaxStreamDataBidiRemote,
self.stream_data_bidi_remote,
limits.stream_data_bidi_remote,
)
put(MaxStreamDataUni, self.stream_data_uni, limits.stream_data_uni)
put(MaxStreamsBidi, self.streams_bidi, limits.streams_bidi)
put(MaxStreamsUni, self.streams_uni, limits.streams_uni)
put(AckDelayExponent, self.ack_delay_exponent, limits.ack_delay_exponent)
put(MaxAckDelay, self.max_ack_delay, limits.max_ack_delay)
put(
ActiveConnectionIdLimit,
self.active_connection_ids,
limits.active_connection_ids,
)
put(MaxDatagramFrameSize, self.datagram_frame, limits.datagram_frame)
if !self.migration {
out.push(Param::flag(DisableActiveMigration))
}
out
}
///|
pub extend Kind with Debug::{to_repr}
///|
pub extend Kind with Eq::{not_equal, equal}
///|
pub extend Param with Debug::{to_repr}
///|
pub extend Param with Eq::{not_equal, equal}
///|
pub extend Limits with Debug::{to_repr}
///|
pub extend Limits with Eq::{not_equal, equal}
///|
/// The block a TLS extension list carries (RFC 9001 §8.2), empty when the extension is
/// absent or its block is truncated: an endpoint with no block falls back to RFC 9000
/// §18.2's defaults, which is what an empty list reads as.
pub fn of_extensions(extensions : ArrayView[@ext.Ext]) -> Array[Param] {
match @ext.find(extensions, QuicTransportParameters) {
Some(e) =>
match decode(e.data[:]) {
Some(params) => params
None => []
}
None => []
}
}
///|
/// The extension a block rides in. RFC 9001 §8.2 makes it mandatory for both endpoints,
/// and `moontls` carries it without reading it, because the contents are QUIC's.
pub fn extension(params : ArrayView[Param]) -> @ext.Ext {
{ kind: QuicTransportParameters, data: encode(params), }
}