use core::fmt;
use crate::limits::Limits;
use crate::varint::VarInt;
use crate::webtransport;
const ID_QPACK_MAX_TABLE_CAPACITY: u64 = 0x01;
const ID_MAX_FIELD_SECTION_SIZE: u64 = 0x06;
const ID_QPACK_BLOCKED_STREAMS: u64 = 0x07;
const ID_ENABLE_CONNECT_PROTOCOL: u64 = 0x08;
const ID_H3_DATAGRAM: u64 = 0x33;
const ID_WT_INITIAL_MAX_DATA: u64 = 0x2b61;
const ID_WT_INITIAL_MAX_STREAMS_UNI: u64 = 0x2b64;
const ID_WT_INITIAL_MAX_STREAMS_BIDI: u64 = 0x2b65;
const ID_ENABLE_WEBTRANSPORT_DRAFT02: u64 = 0x2b603742;
const ID_WT_MAX_SESSIONS_DRAFT14: u64 = 0x14e9cd29;
const ID_WT_ENABLED: u64 = 0x2c7cf000;
const ID_WEBTRANSPORT_MAX_SESSIONS_DRAFT07: u64 = 0xc671706a;
pub const fn is_http2_only_id(id: u64) -> bool {
matches!(id, 0x02..=0x05)
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Setting {
QpackMaxTableCapacity(VarInt),
MaxFieldSectionSize(VarInt),
QpackBlockedStreams(VarInt),
EnableConnectProtocol(bool),
H3Datagram(bool),
WtEnabled(VarInt),
WtMaxSessionsDraft14(VarInt),
EnableWebTransportDraft02(bool),
WebTransportMaxSessionsDraft07(VarInt),
WtInitialMaxData(VarInt),
WtInitialMaxStreamsUni(VarInt),
WtInitialMaxStreamsBidi(VarInt),
Unknown(UnknownSetting),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnknownSetting {
id: VarInt,
value: VarInt,
}
impl UnknownSetting {
pub(crate) const fn new(id: VarInt, value: VarInt) -> Self {
Self { id, value }
}
pub fn id(&self) -> VarInt {
self.id
}
pub fn value(&self) -> VarInt {
self.value
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SettingError {
Http2OnlyId {
id: VarInt,
},
ReservedId {
id: VarInt,
},
InvalidBooleanValue {
id: VarInt,
value: VarInt,
},
DuplicateId {
id: VarInt,
},
}
impl fmt::Display for SettingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Http2OnlyId { id } => {
write!(f, "http/2 only settings id received: {:#x}", id.get())
}
Self::ReservedId { id } => {
write!(f, "reserved settings id received: {:#x}", id.get())
}
Self::InvalidBooleanValue { id, value } => write!(
f,
"boolean settings {:#x} has invalid value {}",
id.get(),
value.get()
),
Self::DuplicateId { id } => {
write!(f, "duplicate settings id: {:#x}", id.get())
}
}
}
}
impl core::error::Error for SettingError {}
impl Setting {
pub fn from_wire(id: VarInt, value: VarInt) -> Result<Self, SettingError> {
let raw = id.get();
if is_http2_only_id(raw) {
return Err(SettingError::Http2OnlyId { id });
}
if raw == 0x00 {
return Err(SettingError::ReservedId { id });
}
let setting = match raw {
ID_QPACK_MAX_TABLE_CAPACITY => Self::QpackMaxTableCapacity(value),
ID_MAX_FIELD_SECTION_SIZE => Self::MaxFieldSectionSize(value),
ID_QPACK_BLOCKED_STREAMS => Self::QpackBlockedStreams(value),
ID_ENABLE_CONNECT_PROTOCOL => Self::EnableConnectProtocol(check_bool(id, value)?),
ID_H3_DATAGRAM => Self::H3Datagram(check_bool(id, value)?),
ID_WT_ENABLED => Self::WtEnabled(value),
ID_WT_MAX_SESSIONS_DRAFT14 => Self::WtMaxSessionsDraft14(value),
ID_ENABLE_WEBTRANSPORT_DRAFT02 => {
Self::EnableWebTransportDraft02(check_bool(id, value)?)
}
ID_WEBTRANSPORT_MAX_SESSIONS_DRAFT07 => Self::WebTransportMaxSessionsDraft07(value),
ID_WT_INITIAL_MAX_DATA => Self::WtInitialMaxData(value),
ID_WT_INITIAL_MAX_STREAMS_UNI => Self::WtInitialMaxStreamsUni(value),
ID_WT_INITIAL_MAX_STREAMS_BIDI => Self::WtInitialMaxStreamsBidi(value),
_ => Self::Unknown(UnknownSetting::new(id, value)),
};
Ok(setting)
}
pub fn as_wire(self) -> (VarInt, VarInt) {
let bool_v = |b: bool| {
if b {
VarInt::from_static(1)
} else {
VarInt::ZERO
}
};
match self {
Self::QpackMaxTableCapacity(v) => (VarInt::from_static(ID_QPACK_MAX_TABLE_CAPACITY), v),
Self::MaxFieldSectionSize(v) => (VarInt::from_static(ID_MAX_FIELD_SECTION_SIZE), v),
Self::QpackBlockedStreams(v) => (VarInt::from_static(ID_QPACK_BLOCKED_STREAMS), v),
Self::EnableConnectProtocol(b) => {
(VarInt::from_static(ID_ENABLE_CONNECT_PROTOCOL), bool_v(b))
}
Self::H3Datagram(b) => (VarInt::from_static(ID_H3_DATAGRAM), bool_v(b)),
Self::WtEnabled(v) => (VarInt::from_static(ID_WT_ENABLED), v),
Self::WtMaxSessionsDraft14(v) => (VarInt::from_static(ID_WT_MAX_SESSIONS_DRAFT14), v),
Self::EnableWebTransportDraft02(b) => (
VarInt::from_static(ID_ENABLE_WEBTRANSPORT_DRAFT02),
bool_v(b),
),
Self::WebTransportMaxSessionsDraft07(v) => {
(VarInt::from_static(ID_WEBTRANSPORT_MAX_SESSIONS_DRAFT07), v)
}
Self::WtInitialMaxData(v) => (VarInt::from_static(ID_WT_INITIAL_MAX_DATA), v),
Self::WtInitialMaxStreamsUni(v) => {
(VarInt::from_static(ID_WT_INITIAL_MAX_STREAMS_UNI), v)
}
Self::WtInitialMaxStreamsBidi(v) => {
(VarInt::from_static(ID_WT_INITIAL_MAX_STREAMS_BIDI), v)
}
Self::Unknown(u) => (u.id(), u.value()),
}
}
pub fn id(self) -> VarInt {
self.as_wire().0
}
}
fn check_bool(id: VarInt, value: VarInt) -> Result<bool, SettingError> {
match value.get() {
0 => Ok(false),
1 => Ok(true),
_ => Err(SettingError::InvalidBooleanValue { id, value }),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Settings {
pub qpack_max_table_capacity: Option<VarInt>,
pub max_field_section_size: Option<VarInt>,
pub qpack_blocked_streams: Option<VarInt>,
pub enable_connect_protocol: Option<bool>,
pub h3_datagram: Option<bool>,
pub wt_settings: Option<webtransport::Settings>,
}
impl Default for Settings {
fn default() -> Self {
Self::new()
}
}
impl Settings {
pub const fn new() -> Self {
Self {
qpack_max_table_capacity: None,
max_field_section_size: None,
qpack_blocked_streams: None,
enable_connect_protocol: None,
h3_datagram: None,
wt_settings: None,
}
}
pub fn from_limits(limits: &Limits) -> Result<Self, crate::varint::VarIntError> {
Ok(Self {
qpack_max_table_capacity: Some(VarInt::new(limits.qpack_max_table_capacity)?),
max_field_section_size: Some(VarInt::new(limits.max_field_section_size)?),
qpack_blocked_streams: Some(VarInt::new(limits.qpack_blocked_streams)?),
enable_connect_protocol: None,
h3_datagram: None,
wt_settings: None,
})
}
pub fn qpack_max_table_capacity(mut self, capacity: VarInt) -> Self {
self.qpack_max_table_capacity = Some(capacity);
self
}
pub fn max_field_section_size(mut self, size: VarInt) -> Self {
self.max_field_section_size = Some(size);
self
}
pub fn qpack_blocked_streams(mut self, streams: VarInt) -> Self {
self.qpack_blocked_streams = Some(streams);
self
}
pub fn enable_connect_protocol(mut self, enable: bool) -> Self {
self.enable_connect_protocol = Some(enable);
self
}
pub fn h3_datagram(mut self, enable: bool) -> Self {
self.h3_datagram = Some(enable);
self
}
pub fn enable_webtransport_server(mut self, wt: webtransport::Settings) -> Self {
self.enable_connect_protocol = Some(true);
self.h3_datagram = Some(true);
self.wt_settings = Some(wt);
self
}
pub fn enable_webtransport_client(mut self, wt: webtransport::Settings) -> Self {
self.h3_datagram = Some(true);
self.wt_settings = Some(wt);
self
}
pub fn is_webtransport_enabled(&self) -> bool {
self.wt_settings.as_ref().is_some_and(|wt| wt.is_enabled())
}
pub fn webtransport_draft_pattern(&self) -> Option<webtransport::DraftVersion> {
self.wt_settings
.as_ref()
.and_then(|wt| wt.detect_draft_pattern())
}
pub fn from_payload(payload: &crate::frame::SettingsPayload) -> Self {
let mut settings = Self::new();
for setting in payload.settings() {
match *setting {
Setting::QpackMaxTableCapacity(v) => {
settings.qpack_max_table_capacity = Some(v);
}
Setting::MaxFieldSectionSize(v) => {
settings.max_field_section_size = Some(v);
}
Setting::QpackBlockedStreams(v) => {
settings.qpack_blocked_streams = Some(v);
}
Setting::EnableConnectProtocol(b) => {
settings.enable_connect_protocol = Some(b);
}
Setting::H3Datagram(b) => {
settings.h3_datagram = Some(b);
}
Setting::WtEnabled(_)
| Setting::WtMaxSessionsDraft14(_)
| Setting::EnableWebTransportDraft02(_)
| Setting::WebTransportMaxSessionsDraft07(_)
| Setting::WtInitialMaxData(_)
| Setting::WtInitialMaxStreamsUni(_)
| Setting::WtInitialMaxStreamsBidi(_) => {}
Setting::Unknown(_) => {}
}
}
settings.wt_settings = webtransport::Settings::from_payload(payload.settings());
settings
}
pub fn iter(&self) -> impl Iterator<Item = Setting> + '_ {
let entries = [
self.qpack_max_table_capacity
.map(Setting::QpackMaxTableCapacity),
self.max_field_section_size
.map(Setting::MaxFieldSectionSize),
self.qpack_blocked_streams.map(Setting::QpackBlockedStreams),
self.enable_connect_protocol
.map(Setting::EnableConnectProtocol),
self.h3_datagram.map(Setting::H3Datagram),
];
entries.into_iter().flatten()
}
pub fn len(&self) -> usize {
let h3_count = self.iter().count();
let wt_count = self
.wt_settings
.as_ref()
.map(|wt| wt.iter().count())
.unwrap_or(0);
h3_count + wt_count
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
fn v(n: u64) -> VarInt {
VarInt::new(n).unwrap()
}
#[test]
fn test_is_http2_only_id() {
assert!(is_http2_only_id(0x02));
assert!(is_http2_only_id(0x03));
assert!(is_http2_only_id(0x04));
assert!(is_http2_only_id(0x05));
assert!(!is_http2_only_id(0x00));
assert!(!is_http2_only_id(0x01));
assert!(!is_http2_only_id(0x06));
}
#[test]
fn test_setting_from_wire_known_ids() {
assert_eq!(
Setting::from_wire(v(0x01), v(4096)).unwrap(),
Setting::QpackMaxTableCapacity(v(4096))
);
assert_eq!(
Setting::from_wire(v(0x06), v(16384)).unwrap(),
Setting::MaxFieldSectionSize(v(16384))
);
assert_eq!(
Setting::from_wire(v(0x07), v(50)).unwrap(),
Setting::QpackBlockedStreams(v(50))
);
assert_eq!(
Setting::from_wire(v(0x08), v(1)).unwrap(),
Setting::EnableConnectProtocol(true)
);
assert_eq!(
Setting::from_wire(v(0x33), v(0)).unwrap(),
Setting::H3Datagram(false)
);
}
#[test]
fn test_setting_from_wire_http2_only_rejected() {
for id in [0x02u64, 0x03, 0x04, 0x05] {
let err = Setting::from_wire(v(id), v(0)).unwrap_err();
assert_eq!(err, SettingError::Http2OnlyId { id: v(id) });
}
}
#[test]
fn test_setting_from_wire_reserved_id_rejected() {
let err = Setting::from_wire(v(0x00), v(0)).unwrap_err();
assert_eq!(err, SettingError::ReservedId { id: v(0x00) });
}
#[test]
fn test_setting_from_wire_invalid_boolean() {
let err = Setting::from_wire(v(0x08), v(2)).unwrap_err();
assert_eq!(
err,
SettingError::InvalidBooleanValue {
id: v(0x08),
value: v(2)
}
);
let err = Setting::from_wire(v(0x33), v(u64::from(u8::MAX))).unwrap_err();
assert_eq!(
err,
SettingError::InvalidBooleanValue {
id: v(0x33),
value: v(u64::from(u8::MAX))
}
);
}
#[test]
fn test_setting_from_wire_unknown_id() {
let s = Setting::from_wire(v(0x99), v(42)).unwrap();
let Setting::Unknown(u) = s else {
panic!("expected Unknown");
};
assert_eq!(u.id(), v(0x99));
assert_eq!(u.value(), v(42));
}
#[test]
fn test_setting_from_wire_wt_ids() {
assert_eq!(
Setting::from_wire(v(0x2c7cf000), v(1)).unwrap(),
Setting::WtEnabled(v(1))
);
assert_eq!(
Setting::from_wire(v(0x14e9cd29), v(1)).unwrap(),
Setting::WtMaxSessionsDraft14(v(1))
);
assert_eq!(
Setting::from_wire(v(0x2b603742), v(1)).unwrap(),
Setting::EnableWebTransportDraft02(true)
);
assert_eq!(
Setting::from_wire(v(0xc671706a), v(3)).unwrap(),
Setting::WebTransportMaxSessionsDraft07(v(3))
);
assert_eq!(
Setting::from_wire(v(0x2b61), v(1024)).unwrap(),
Setting::WtInitialMaxData(v(1024))
);
assert_eq!(
Setting::from_wire(v(0x2b64), v(100)).unwrap(),
Setting::WtInitialMaxStreamsUni(v(100))
);
assert_eq!(
Setting::from_wire(v(0x2b65), v(50)).unwrap(),
Setting::WtInitialMaxStreamsBidi(v(50))
);
}
#[test]
fn test_setting_as_wire_roundtrip() {
let cases = [
Setting::QpackMaxTableCapacity(v(4096)),
Setting::MaxFieldSectionSize(v(16384)),
Setting::QpackBlockedStreams(v(100)),
Setting::EnableConnectProtocol(true),
Setting::EnableConnectProtocol(false),
Setting::H3Datagram(true),
Setting::H3Datagram(false),
Setting::WtEnabled(v(1)),
Setting::WtMaxSessionsDraft14(v(7)),
Setting::EnableWebTransportDraft02(true),
Setting::WebTransportMaxSessionsDraft07(v(3)),
Setting::WtInitialMaxData(v(1024)),
Setting::WtInitialMaxStreamsUni(v(100)),
Setting::WtInitialMaxStreamsBidi(v(50)),
Setting::from_wire(v(0xdead_beef), v(42)).unwrap(),
];
for setting in cases {
let (id, value) = setting.as_wire();
let restored = Setting::from_wire(id, value).unwrap();
assert_eq!(restored, setting);
}
}
#[test]
fn test_setting_id() {
assert_eq!(Setting::QpackMaxTableCapacity(v(4096)).id(), v(0x01));
assert_eq!(Setting::H3Datagram(true).id(), v(0x33));
let unknown = Setting::from_wire(v(0xfeed), v(0)).unwrap();
assert_eq!(unknown.id(), v(0xfeed));
}
#[test]
fn test_setting_error_display() {
let err = SettingError::Http2OnlyId { id: v(0x02) };
let s = format!("{err}");
assert!(s.contains("0x2"));
let err = SettingError::ReservedId { id: v(0x00) };
let s = format!("{err}");
assert!(s.contains("0x0"));
let err = SettingError::InvalidBooleanValue {
id: v(0x08),
value: v(2),
};
let s = format!("{err}");
assert!(s.contains("0x8"));
assert!(s.contains('2'));
}
#[test]
fn test_settings_default() {
let settings = Settings::default();
assert!(settings.is_empty());
}
#[test]
fn test_settings_builder() {
let settings = Settings::new()
.qpack_max_table_capacity(v(4096))
.max_field_section_size(v(16384))
.qpack_blocked_streams(v(100))
.enable_connect_protocol(true)
.h3_datagram(false);
assert_eq!(settings.qpack_max_table_capacity, Some(v(4096)));
assert_eq!(settings.max_field_section_size, Some(v(16384)));
assert_eq!(settings.qpack_blocked_streams, Some(v(100)));
assert_eq!(settings.enable_connect_protocol, Some(true));
assert_eq!(settings.h3_datagram, Some(false));
assert_eq!(settings.len(), 5);
}
#[test]
fn test_settings_from_limits() {
let limits = Limits::new()
.qpack_max_table_capacity(4096)
.max_field_section_size(32768)
.qpack_blocked_streams(50);
let settings = Settings::from_limits(&limits).unwrap();
assert_eq!(settings.qpack_max_table_capacity, Some(v(4096)));
assert_eq!(settings.max_field_section_size, Some(v(32768)));
assert_eq!(settings.qpack_blocked_streams, Some(v(50)));
}
#[test]
fn test_settings_iter() {
let settings = Settings::new()
.qpack_max_table_capacity(v(4096))
.max_field_section_size(v(16384));
let entries: Vec<_> = settings.iter().collect();
assert_eq!(entries.len(), 2);
assert!(entries.contains(&Setting::QpackMaxTableCapacity(v(4096))));
assert!(entries.contains(&Setting::MaxFieldSectionSize(v(16384))));
}
#[test]
fn test_enable_webtransport() {
let wt = webtransport::Settings::new()
.wt_enabled(v(1))
.enable_webtransport_draft02(true)
.webtransport_max_sessions_draft07(v(1))
.wt_initial_max_streams_bidi(v(100))
.wt_initial_max_streams_uni(v(100))
.wt_initial_max_data(v(1_048_576));
let settings = Settings::new().enable_webtransport_server(wt);
assert_eq!(settings.enable_connect_protocol, Some(true));
assert_eq!(settings.h3_datagram, Some(true));
assert!(settings.is_webtransport_enabled());
let wt = settings.wt_settings.unwrap();
assert_eq!(wt.wt_enabled, v(1));
assert_eq!(wt.enable_webtransport_draft02, Some(true));
assert_eq!(wt.webtransport_max_sessions_draft07, Some(v(1)));
assert_eq!(wt.wt_initial_max_streams_bidi, v(100));
assert_eq!(wt.wt_initial_max_streams_uni, v(100));
assert_eq!(wt.wt_initial_max_data, v(1_048_576));
}
#[test]
fn test_is_webtransport_enabled() {
let settings = Settings::new();
assert!(!settings.is_webtransport_enabled());
let wt = webtransport::Settings::new().wt_enabled(v(1));
let settings = Settings::new().enable_webtransport_server(wt);
assert!(settings.is_webtransport_enabled());
}
#[test]
fn test_len_includes_wt_settings() {
let wt = webtransport::Settings::new()
.wt_enabled(v(1))
.wt_initial_max_streams_bidi(v(100));
let settings = Settings::new()
.qpack_max_table_capacity(v(4096))
.enable_webtransport_server(wt);
assert_eq!(settings.len(), 5);
}
#[test]
fn test_add_duplicate_rejected_at_payload() {
use crate::frame::SettingsPayload;
let mut payload = SettingsPayload::new();
payload
.add(Setting::QpackMaxTableCapacity(v(4096)))
.unwrap();
let err = payload
.add(Setting::QpackMaxTableCapacity(v(8192)))
.unwrap_err();
assert_eq!(err, SettingError::DuplicateId { id: v(0x01) });
}
#[test]
fn test_from_payload_ignores_unknown() {
use crate::frame::SettingsPayload;
let unknown = Setting::from_wire(v(0xdead), v(1)).unwrap();
let mut payload = SettingsPayload::new();
payload.add(unknown).unwrap();
payload.add(Setting::H3Datagram(true)).unwrap();
let settings = Settings::from_payload(&payload);
assert_eq!(settings.h3_datagram, Some(true));
assert!(settings.qpack_max_table_capacity.is_none());
}
#[test]
fn test_setting_error_duplicate_id_display() {
let err = SettingError::DuplicateId { id: v(0x01) };
let s = format!("{err}");
assert!(s.contains("duplicate"));
assert!(s.contains("0x1"));
}
#[test]
fn test_from_limits_out_of_range_qpack_max_table_capacity() {
let mut limits = Limits::new();
limits.qpack_max_table_capacity = 1u64 << 62;
let err = Settings::from_limits(&limits).unwrap_err();
assert!(matches!(err, crate::varint::VarIntError::OutOfRange { .. }));
}
#[test]
fn test_from_limits_out_of_range_max_field_section_size() {
let mut limits = Limits::new();
limits.max_field_section_size = 1u64 << 62;
let err = Settings::from_limits(&limits).unwrap_err();
assert!(matches!(err, crate::varint::VarIntError::OutOfRange { .. }));
}
#[test]
fn test_from_limits_out_of_range_qpack_blocked_streams() {
let mut limits = Limits::new();
limits.qpack_blocked_streams = 1u64 << 62;
let err = Settings::from_limits(&limits).unwrap_err();
assert!(matches!(err, crate::varint::VarIntError::OutOfRange { .. }));
}
}