open-standards/joule-code/joule-code-rs/crates/joule-moq/src/draft.rs
Transaction Science 32cffa4086 joule-moq: drafts chosen by this crate's preference; relay tests and client take the draft
Draft::select takes the most preferred draft a client offers (-22 first),
as the relay's TLS does. The relay client takes extra Setup Options
(AUTHORITY and PATH for a moqt:// URI), and the relay tests can run any
session at -18 or -16.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 05:33:13 -04:00

202 lines
8.2 KiB
Rust

//! Which MOQT draft a session speaks.
//!
//! Since -15 a draft is negotiated before the first MOQT byte: by ALPN on
//! native QUIC and by `WT-Available-Protocols` on WebTransport, each draft
//! named `moqt-NN` (`draft-ietf-moq-transport-22` §6.2, `-18` §3.1, `-16` §3.1).
//! No two drafts interoperate, so a session is one draft from its first byte to
//! its last.
//!
//! This crate speaks three drafts, each a codec module of its own:
//!
//! | draft | ALPN | module | role |
//! |---|---|---|---|
//! | `draft-ietf-moq-transport-22` | `moqt-22` | [`crate::draft22`] | the default, and the model the rest of the crate is written in |
//! | `draft-ietf-moq-transport-18` | `moqt-18` | [`crate::draft18`] | the working group's interoperability target |
//! | `draft-ietf-moq-transport-16` | `moqt-16` | [`crate::draft16`] | deployed public relays |
//!
//! A session at -18 or -16 is carried by translating each message and object to
//! and from the -22 model at the session's edge (`draft18::bridge`,
//! `draft16::bridge`); every place where the drafts differ is named there, and
//! what one draft can say and the other cannot is refused there by name rather
//! than guessed at.
/// A MOQT draft this crate can speak.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
pub enum Draft {
/// `draft-ietf-moq-transport-16`, 13 January 2026.
D16,
/// `draft-ietf-moq-transport-18`, 12 May 2026.
D18,
/// `draft-ietf-moq-transport-22`, 1 October 2026.
D22,
}
impl Draft {
/// Every draft this crate speaks, most preferred first: -22 is the default,
/// and an earlier draft is chosen only when the peer offers no later one.
pub const SUPPORTED: &'static [Draft] = &[Draft::D22, Draft::D18, Draft::D16];
/// The draft a session speaks when nothing else is asked for.
pub const DEFAULT: Draft = Draft::D22;
/// The draft number.
pub const fn number(self) -> u32 {
match self {
Draft::D16 => 16,
Draft::D18 => 18,
Draft::D22 => 22,
}
}
/// The ALPN token (and WebTransport protocol) that selects it.
pub const fn alpn(self) -> &'static [u8] {
match self {
Draft::D16 => crate::draft16::ALPN,
Draft::D18 => crate::draft18::ALPN,
Draft::D22 => crate::draft22::ALPN,
}
}
/// The ALPN token as text, the form `WT-Available-Protocols` carries.
pub fn alpn_str(self) -> &'static str {
std::str::from_utf8(self.alpn()).expect("ALPN tokens are ASCII")
}
/// The conventional 32-bit code of the draft, `0xff000000 + NN`.
pub const fn version_code(self) -> u32 {
0xff00_0000 + self.number()
}
pub fn from_alpn(alpn: &[u8]) -> Option<Draft> {
Self::SUPPORTED.iter().copied().find(|d| d.alpn() == alpn)
}
pub fn from_version_code(code: u32) -> Option<Draft> {
Self::SUPPORTED.iter().copied().find(|d| d.version_code() == code)
}
/// The draft of its number, when this crate speaks it.
pub fn from_number(n: u32) -> Option<Draft> {
Self::SUPPORTED.iter().copied().find(|d| d.number() == n)
}
/// The draft a server chooses among a client's ALPNs: the one this crate prefers
/// most ([`Draft::SUPPORTED`]'s order) of those offered. The relay's TLS
/// configuration chooses the same way (the server's list first).
pub fn select<'a>(offered: impl IntoIterator<Item = &'a [u8]>) -> Option<Draft> {
let offered: Vec<Draft> = offered.into_iter().filter_map(Draft::from_alpn).collect();
Self::SUPPORTED.iter().copied().find(|d| offered.contains(d))
}
/// The ALPN list a client offers, most preferred first.
pub fn alpn_list() -> Vec<Vec<u8>> {
Self::SUPPORTED.iter().map(|d| d.alpn().to_vec()).collect()
}
}
impl std::fmt::Display for Draft {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "draft-ietf-moq-transport-{}", self.number())
}
}
/// Whether `v` follows the grease pattern `0x7f * N + 0x9D` (-22 §13, -18 §14).
pub fn is_grease(v: u64) -> bool {
v >= 0x9d && (v - 0x9d).is_multiple_of(0x7f)
}
/// A registry of status or error codes: a newtype over the wire value, so a code a
/// draft does not name still round-trips, with the names that draft registers.
macro_rules! code_registry {
(
$(#[$meta:meta])*
$name:ident, internal = $internal:expr, { $( $(#[$cm:meta])* $konst:ident = $val:expr ),* $(,)? }
) => {
$(#[$meta])*
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
pub struct $name(pub u64);
impl $name {
$( $(#[$cm])* pub const $konst: $name = $name($val); )*
/// Every code this draft registers, with its name.
pub const REGISTERED: &'static [($name, &'static str)] = &[ $( ($name($val), stringify!($konst)) ),* ];
/// The registered name, or `None` for a code this draft does not register.
pub fn name(self) -> Option<&'static str> {
Self::REGISTERED.iter().find(|(c, _)| *c == self).map(|(_, n)| *n)
}
/// The code read as the draft says an unknown one reads: `INTERNAL_ERROR`
/// for a code it does not register (-22 §13, -18 §14).
pub fn effective(self) -> $name {
if self.name().is_some() { self } else { $internal }
}
/// Whether the value is a grease value (`0x7f * N + 0x9D`).
pub fn is_grease(self) -> bool {
$crate::draft::is_grease(self.0)
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.name() {
Some(n) => f.write_str(n),
None => write!(f, "{:#x}", self.0),
}
}
}
};
}
pub(crate) use code_registry;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn each_draft_by_alpn_and_code() {
assert_eq!(Draft::D22.alpn(), b"moqt-22");
assert_eq!(Draft::D18.alpn(), b"moqt-18");
assert_eq!(Draft::D16.alpn(), b"moqt-16");
assert_eq!(Draft::D22.version_code(), 0xff00_0016);
assert_eq!(Draft::D18.version_code(), 0xff00_0012);
assert_eq!(Draft::D16.version_code(), 0xff00_0010);
assert_eq!(Draft::from_alpn(b"moqt-22"), Some(Draft::D22));
assert_eq!(Draft::from_alpn(b"moqt-18"), Some(Draft::D18));
assert_eq!(Draft::from_alpn(b"moqt-16"), Some(Draft::D16));
// Drafts this crate does not speak, and the ALPN every draft before -15 used.
for other in [&b"moqt-17"[..], b"moqt-20", b"moqt-14", b"moq-00", b"h3", b"moqt"] {
assert_eq!(Draft::from_alpn(other), None, "{}", String::from_utf8_lossy(other));
}
assert_eq!(Draft::from_version_code(crate::draft22::VERSION), Some(Draft::D22));
assert_eq!(Draft::from_version_code(crate::draft18::VERSION), Some(Draft::D18));
assert_eq!(Draft::from_version_code(crate::draft16::VERSION), Some(Draft::D16));
assert_eq!(Draft::from_number(18), Some(Draft::D18));
assert_eq!(Draft::D18.to_string(), "draft-ietf-moq-transport-18");
}
#[test]
fn negotiation_prefers_22() {
// A server takes the draft it prefers most of those the client offers.
let offered: [&[u8]; 3] = [b"moqt-18", b"h3", b"moqt-22"];
assert_eq!(Draft::select(offered), Some(Draft::D22));
let offered: [&[u8]; 3] = [b"moqt-20", b"moqt-16", b"moqt-18"];
assert_eq!(Draft::select(offered), Some(Draft::D18));
let offered: [&[u8]; 2] = [b"moqt-20", b"moqt-16"];
assert_eq!(Draft::select(offered), Some(Draft::D16));
let offered: [&[u8]; 2] = [b"moqt-14", b"moq-00"];
assert_eq!(Draft::select(offered), None);
// This crate offers -22 first.
assert_eq!(Draft::alpn_list(), vec![b"moqt-22".to_vec(), b"moqt-18".to_vec(), b"moqt-16".to_vec()]);
assert_eq!(Draft::DEFAULT, Draft::D22);
assert_eq!(Draft::SUPPORTED[0], Draft::DEFAULT);
}
#[test]
fn grease() {
assert!(is_grease(0x9d) && is_grease(0x11c));
assert!(!is_grease(0x9c));
}
}