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