Skip to main content

moqtap_codec/
version.rs

1//! MoQT draft version enum for runtime dispatch.
2
3use crate::varint::VarInt;
4
5/// MoQT draft version for runtime codec selection.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum DraftVersion {
8    /// draft-ietf-moq-transport-07.
9    Draft07,
10    /// draft-ietf-moq-transport-08.
11    Draft08,
12    /// draft-ietf-moq-transport-09.
13    Draft09,
14    /// draft-ietf-moq-transport-10.
15    Draft10,
16    /// draft-ietf-moq-transport-11.
17    Draft11,
18    /// draft-ietf-moq-transport-12.
19    Draft12,
20    /// draft-ietf-moq-transport-13.
21    Draft13,
22    /// draft-ietf-moq-transport-14.
23    Draft14,
24    /// draft-ietf-moq-transport-15.
25    Draft15,
26    /// draft-ietf-moq-transport-16.
27    Draft16,
28    /// draft-ietf-moq-transport-17.
29    Draft17,
30    /// draft-ietf-moq-transport-18.
31    Draft18,
32}
33
34impl DraftVersion {
35    /// The MoQT version number announced in CLIENT_SETUP.
36    ///
37    /// Format: `0xff000000 + draft_number`. Draft-15+ use ALPN for version
38    /// negotiation and may not include a version in CLIENT_SETUP at all.
39    pub fn version_varint(&self) -> VarInt {
40        let n = match self {
41            DraftVersion::Draft07 => 7,
42            DraftVersion::Draft08 => 8,
43            DraftVersion::Draft09 => 9,
44            DraftVersion::Draft10 => 10,
45            DraftVersion::Draft11 => 11,
46            DraftVersion::Draft12 => 12,
47            DraftVersion::Draft13 => 13,
48            DraftVersion::Draft14 => 14,
49            DraftVersion::Draft15 => 15,
50            DraftVersion::Draft16 => 16,
51            DraftVersion::Draft17 => 17,
52            DraftVersion::Draft18 => 18,
53        };
54        VarInt::from_usize(0xff000000 + n as usize)
55    }
56
57    /// The ALPN protocol identifier for raw QUIC connections.
58    ///
59    /// Drafts 07–14 all use `moq-00` and negotiate the draft version in
60    /// CLIENT_SETUP / SERVER_SETUP. Draft-15+ encode the draft number in the
61    /// ALPN itself (`moqt-<N>`), per §3.1.2 of each spec.
62    pub fn quic_alpn(&self) -> &'static [u8] {
63        match self {
64            DraftVersion::Draft07
65            | DraftVersion::Draft08
66            | DraftVersion::Draft09
67            | DraftVersion::Draft10
68            | DraftVersion::Draft11
69            | DraftVersion::Draft12
70            | DraftVersion::Draft13
71            | DraftVersion::Draft14 => b"moq-00",
72            DraftVersion::Draft15 => b"moqt-15",
73            DraftVersion::Draft16 => b"moqt-16",
74            DraftVersion::Draft17 => b"moqt-17",
75            DraftVersion::Draft18 => b"moqt-18",
76        }
77    }
78
79    /// Resolve an ALPN identifier to a specific draft version.
80    ///
81    /// Returns `Some` for ALPNs that unambiguously identify a draft
82    /// (`moqt-15`, `moqt-16`, `moqt-17`, `moqt-18`). Returns `None` for
83    /// `moq-00` — which covers drafts 07–14 and requires inspecting
84    /// CLIENT_SETUP's supported-versions list — and for any unrecognized
85    /// ALPN.
86    pub fn from_alpn(alpn: &[u8]) -> Option<DraftVersion> {
87        match alpn {
88            b"moqt-15" => Some(DraftVersion::Draft15),
89            b"moqt-16" => Some(DraftVersion::Draft16),
90            b"moqt-17" => Some(DraftVersion::Draft17),
91            b"moqt-18" => Some(DraftVersion::Draft18),
92            _ => None,
93        }
94    }
95
96    /// Resolve a draft number (e.g. 7..=18) to a `DraftVersion`.
97    ///
98    /// Returns `None` for numbers outside the supported range.
99    pub fn from_number(n: u8) -> Option<DraftVersion> {
100        match n {
101            7 => Some(DraftVersion::Draft07),
102            8 => Some(DraftVersion::Draft08),
103            9 => Some(DraftVersion::Draft09),
104            10 => Some(DraftVersion::Draft10),
105            11 => Some(DraftVersion::Draft11),
106            12 => Some(DraftVersion::Draft12),
107            13 => Some(DraftVersion::Draft13),
108            14 => Some(DraftVersion::Draft14),
109            15 => Some(DraftVersion::Draft15),
110            16 => Some(DraftVersion::Draft16),
111            17 => Some(DraftVersion::Draft17),
112            18 => Some(DraftVersion::Draft18),
113            _ => None,
114        }
115    }
116
117    /// Whether this draft uses a 16-bit big-endian message length in control
118    /// message framing (`true`) or a QUIC varint (`false`).
119    ///
120    /// Draft-11 changed the framing from `Length(i)` to `Length(16)`.
121    pub fn uses_fixed_length_framing(&self) -> bool {
122        self.number() >= 11
123    }
124
125    /// The draft number (e.g. 7, 14, 17).
126    pub fn number(&self) -> u8 {
127        match self {
128            DraftVersion::Draft07 => 7,
129            DraftVersion::Draft08 => 8,
130            DraftVersion::Draft09 => 9,
131            DraftVersion::Draft10 => 10,
132            DraftVersion::Draft11 => 11,
133            DraftVersion::Draft12 => 12,
134            DraftVersion::Draft13 => 13,
135            DraftVersion::Draft14 => 14,
136            DraftVersion::Draft15 => 15,
137            DraftVersion::Draft16 => 16,
138            DraftVersion::Draft17 => 17,
139            DraftVersion::Draft18 => 18,
140        }
141    }
142}
143
144impl std::fmt::Display for DraftVersion {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        write!(f, "draft-{:02}", self.number())
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn from_alpn_resolves_drafts_15_plus() {
156        assert_eq!(DraftVersion::from_alpn(b"moqt-15"), Some(DraftVersion::Draft15));
157        assert_eq!(DraftVersion::from_alpn(b"moqt-16"), Some(DraftVersion::Draft16));
158        assert_eq!(DraftVersion::from_alpn(b"moqt-17"), Some(DraftVersion::Draft17));
159        assert_eq!(DraftVersion::from_alpn(b"moqt-18"), Some(DraftVersion::Draft18));
160    }
161
162    #[test]
163    fn from_alpn_none_for_moq_00_and_unknown() {
164        assert_eq!(DraftVersion::from_alpn(b"moq-00"), None);
165        assert_eq!(DraftVersion::from_alpn(b"h3"), None);
166        assert_eq!(DraftVersion::from_alpn(b""), None);
167        assert_eq!(DraftVersion::from_alpn(b"moqt-99"), None);
168    }
169
170    #[test]
171    fn from_alpn_round_trips_with_quic_alpn() {
172        for d in [
173            DraftVersion::Draft15,
174            DraftVersion::Draft16,
175            DraftVersion::Draft17,
176            DraftVersion::Draft18,
177        ] {
178            assert_eq!(DraftVersion::from_alpn(d.quic_alpn()), Some(d));
179        }
180    }
181
182    #[test]
183    fn from_number_resolves_supported_range() {
184        for n in 7..=18u8 {
185            assert!(DraftVersion::from_number(n).is_some(), "draft {n} should resolve");
186        }
187    }
188
189    #[test]
190    fn from_number_none_outside_range() {
191        assert_eq!(DraftVersion::from_number(0), None);
192        assert_eq!(DraftVersion::from_number(6), None);
193        assert_eq!(DraftVersion::from_number(19), None);
194        assert_eq!(DraftVersion::from_number(255), None);
195    }
196}