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