Skip to main content

moqtap_codec/
version.rs

1//! MoQT draft version enum for runtime dispatch.
2
3use crate::varint::{Moqt17, Moqt18, VarInt, VarIntError};
4use bytes::{Buf, BufMut};
5
6/// A variable-length integer encoding used by some MoQT draft.
7///
8/// The MoQT variants are named for the draft that introduced each revision,
9/// not for the drafts that use it — [`DraftVersion::varint_encoding`] is the
10/// one place that maps drafts to encodings.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum VarIntEncoding {
13    /// The QUIC variable-length integer, RFC 9000 Section 16: a two-bit length
14    /// prefix, 1/2/4/8 bytes, values up to 2^62 - 1.
15    Rfc9000,
16    /// MoQT's own, as introduced in draft-17 Section 1.4.1: the length is the
17    /// number of leading 1 bits in the first byte. Draft-17 omits the 7-byte
18    /// length and rejects that code point.
19    Moqt17,
20    /// MoQT's own, as revised in draft-18, which restored the 7-byte length so
21    /// all of 1 to 9 bytes are defined.
22    Moqt18,
23}
24
25/// MoQT draft version for runtime codec selection.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum DraftVersion {
28    /// draft-ietf-moq-transport-07.
29    Draft07,
30    /// draft-ietf-moq-transport-08.
31    Draft08,
32    /// draft-ietf-moq-transport-09.
33    Draft09,
34    /// draft-ietf-moq-transport-10.
35    Draft10,
36    /// draft-ietf-moq-transport-11.
37    Draft11,
38    /// draft-ietf-moq-transport-12.
39    Draft12,
40    /// draft-ietf-moq-transport-13.
41    Draft13,
42    /// draft-ietf-moq-transport-14.
43    Draft14,
44    /// draft-ietf-moq-transport-15.
45    Draft15,
46    /// draft-ietf-moq-transport-16.
47    Draft16,
48    /// draft-ietf-moq-transport-17.
49    Draft17,
50    /// draft-ietf-moq-transport-18.
51    Draft18,
52    /// draft-ietf-moq-transport-19.
53    Draft19,
54}
55
56impl DraftVersion {
57    /// The MoQT version number announced in CLIENT_SETUP.
58    ///
59    /// Format: `0xff000000 + draft_number`. Draft-15+ use ALPN for version
60    /// negotiation and may not include a version in CLIENT_SETUP at all.
61    pub fn version_varint(&self) -> VarInt {
62        let n = match self {
63            DraftVersion::Draft07 => 7,
64            DraftVersion::Draft08 => 8,
65            DraftVersion::Draft09 => 9,
66            DraftVersion::Draft10 => 10,
67            DraftVersion::Draft11 => 11,
68            DraftVersion::Draft12 => 12,
69            DraftVersion::Draft13 => 13,
70            DraftVersion::Draft14 => 14,
71            DraftVersion::Draft15 => 15,
72            DraftVersion::Draft16 => 16,
73            DraftVersion::Draft17 => 17,
74            DraftVersion::Draft18 => 18,
75            DraftVersion::Draft19 => 19,
76        };
77        VarInt::from_usize(0xff000000 + n as usize)
78    }
79
80    /// The ALPN protocol identifier for raw QUIC connections.
81    ///
82    /// Drafts 07–14 all use `moq-00` and negotiate the draft version in
83    /// CLIENT_SETUP / SERVER_SETUP. Draft-15+ encode the draft number in the
84    /// ALPN itself (`moqt-<N>`), so version selection happens during the TLS
85    /// handshake rather than after it.
86    pub fn quic_alpn(&self) -> &'static [u8] {
87        match self {
88            DraftVersion::Draft07
89            | DraftVersion::Draft08
90            | DraftVersion::Draft09
91            | DraftVersion::Draft10
92            | DraftVersion::Draft11
93            | DraftVersion::Draft12
94            | DraftVersion::Draft13
95            | DraftVersion::Draft14 => b"moq-00",
96            DraftVersion::Draft15 => b"moqt-15",
97            DraftVersion::Draft16 => b"moqt-16",
98            DraftVersion::Draft17 => b"moqt-17",
99            DraftVersion::Draft18 => b"moqt-18",
100            DraftVersion::Draft19 => b"moqt-19",
101        }
102    }
103
104    /// Resolve an ALPN identifier to a specific draft version.
105    ///
106    /// Returns `Some` for ALPNs that unambiguously identify a draft
107    /// (`moqt-15`, `moqt-16`, `moqt-17`, `moqt-18`, `moqt-19`). Returns `None`
108    /// for `moq-00` — which covers drafts 07–14 and requires inspecting
109    /// CLIENT_SETUP's supported-versions list — and for any unrecognized
110    /// ALPN.
111    pub fn from_alpn(alpn: &[u8]) -> Option<DraftVersion> {
112        match alpn {
113            b"moqt-15" => Some(DraftVersion::Draft15),
114            b"moqt-16" => Some(DraftVersion::Draft16),
115            b"moqt-17" => Some(DraftVersion::Draft17),
116            b"moqt-18" => Some(DraftVersion::Draft18),
117            b"moqt-19" => Some(DraftVersion::Draft19),
118            _ => None,
119        }
120    }
121
122    /// Resolve a draft number (e.g. 7..=18) to a `DraftVersion`.
123    ///
124    /// Returns `None` for numbers outside the supported range.
125    pub fn from_number(n: u8) -> Option<DraftVersion> {
126        match n {
127            7 => Some(DraftVersion::Draft07),
128            8 => Some(DraftVersion::Draft08),
129            9 => Some(DraftVersion::Draft09),
130            10 => Some(DraftVersion::Draft10),
131            11 => Some(DraftVersion::Draft11),
132            12 => Some(DraftVersion::Draft12),
133            13 => Some(DraftVersion::Draft13),
134            14 => Some(DraftVersion::Draft14),
135            15 => Some(DraftVersion::Draft15),
136            16 => Some(DraftVersion::Draft16),
137            17 => Some(DraftVersion::Draft17),
138            18 => Some(DraftVersion::Draft18),
139            19 => Some(DraftVersion::Draft19),
140            _ => None,
141        }
142    }
143
144    /// Whether this draft uses a 16-bit big-endian message length in control
145    /// message framing (`true`) or a QUIC varint (`false`).
146    ///
147    /// Draft-11 changed the framing from `Length(i)` to `Length(16)`.
148    pub fn uses_fixed_length_framing(&self) -> bool {
149        self.number() >= 11
150    }
151
152    /// Which variable-length integer encoding this draft's wire format uses.
153    ///
154    /// Matched draft by draft rather than derived from the number. The series
155    /// has already changed encoding once mid-stream and revised it again a
156    /// draft later, so there is no rule to extrapolate from: adding a variant
157    /// to [`DraftVersion`] must fail to compile here until someone reads that
158    /// draft and says which encoding it uses.
159    pub fn varint_encoding(&self) -> VarIntEncoding {
160        match self {
161            DraftVersion::Draft07
162            | DraftVersion::Draft08
163            | DraftVersion::Draft09
164            | DraftVersion::Draft10
165            | DraftVersion::Draft11
166            | DraftVersion::Draft12
167            | DraftVersion::Draft13
168            | DraftVersion::Draft14
169            | DraftVersion::Draft15
170            | DraftVersion::Draft16 => VarIntEncoding::Rfc9000,
171            DraftVersion::Draft17 => VarIntEncoding::Moqt17,
172            DraftVersion::Draft18 | DraftVersion::Draft19 => VarIntEncoding::Moqt18,
173        }
174    }
175
176    /// Whether this draft uses one of MoQT's own variable-length integers
177    /// rather than RFC 9000's.
178    pub fn uses_moqt_varint(&self) -> bool {
179        self.varint_encoding() != VarIntEncoding::Rfc9000
180    }
181
182    /// The total encoded length of a variable-length integer, from its first
183    /// byte, under this draft's encoding.
184    ///
185    /// Available without a buffer, because a reader needs it to know how many
186    /// bytes to wait for before it can decode at all. On draft-17 a first byte
187    /// of `11111100` reports 7 even though the draft forbids that length: the
188    /// reader waits for the whole field, then [`Self::decode_varint`] rejects
189    /// it.
190    pub fn varint_len(&self, first_byte: u8) -> usize {
191        match self.varint_encoding() {
192            VarIntEncoding::Rfc9000 => 1 << (first_byte >> 6),
193            VarIntEncoding::Moqt17 | VarIntEncoding::Moqt18 => {
194                if first_byte == 0xFF {
195                    9
196                } else {
197                    first_byte.leading_ones() as usize + 1
198                }
199            }
200        }
201    }
202
203    /// Decode a variable-length integer under this draft's encoding.
204    pub fn decode_varint(&self, buf: &mut impl Buf) -> Result<VarInt, VarIntError> {
205        match self.varint_encoding() {
206            VarIntEncoding::Rfc9000 => VarInt::decode(buf),
207            VarIntEncoding::Moqt17 => VarInt::decode_moqt::<Moqt17>(buf),
208            VarIntEncoding::Moqt18 => VarInt::decode_moqt::<Moqt18>(buf),
209        }
210    }
211
212    /// Encode a variable-length integer under this draft's encoding.
213    pub fn encode_varint(&self, value: VarInt, buf: &mut impl BufMut) {
214        match self.varint_encoding() {
215            VarIntEncoding::Rfc9000 => value.encode(buf),
216            VarIntEncoding::Moqt17 => value.encode_moqt::<Moqt17>(buf),
217            VarIntEncoding::Moqt18 => value.encode_moqt::<Moqt18>(buf),
218        }
219    }
220
221    /// The draft number (e.g. 7, 14, 17).
222    pub fn number(&self) -> u8 {
223        match self {
224            DraftVersion::Draft07 => 7,
225            DraftVersion::Draft08 => 8,
226            DraftVersion::Draft09 => 9,
227            DraftVersion::Draft10 => 10,
228            DraftVersion::Draft11 => 11,
229            DraftVersion::Draft12 => 12,
230            DraftVersion::Draft13 => 13,
231            DraftVersion::Draft14 => 14,
232            DraftVersion::Draft15 => 15,
233            DraftVersion::Draft16 => 16,
234            DraftVersion::Draft17 => 17,
235            DraftVersion::Draft18 => 18,
236            DraftVersion::Draft19 => 19,
237        }
238    }
239}
240
241impl std::fmt::Display for DraftVersion {
242    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243        write!(f, "draft-{:02}", self.number())
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    /// The draft-to-encoding map, stated once so a change to it is a change to
252    /// this list rather than a silent consequence of a comparison.
253    #[test]
254    fn every_draft_states_its_varint_encoding() {
255        use VarIntEncoding::*;
256        let expected = [
257            (DraftVersion::Draft07, Rfc9000),
258            (DraftVersion::Draft08, Rfc9000),
259            (DraftVersion::Draft09, Rfc9000),
260            (DraftVersion::Draft10, Rfc9000),
261            (DraftVersion::Draft11, Rfc9000),
262            (DraftVersion::Draft12, Rfc9000),
263            (DraftVersion::Draft13, Rfc9000),
264            (DraftVersion::Draft14, Rfc9000),
265            (DraftVersion::Draft15, Rfc9000),
266            (DraftVersion::Draft16, Rfc9000),
267            (DraftVersion::Draft17, Moqt17),
268            (DraftVersion::Draft18, Moqt18),
269            (DraftVersion::Draft19, Moqt18),
270        ];
271        for (draft, encoding) in expected {
272            assert_eq!(draft.varint_encoding(), encoding, "{draft}");
273            assert_eq!(draft.uses_moqt_varint(), encoding != Rfc9000, "{draft}");
274        }
275    }
276
277    /// The same value, in the encoding each era actually uses. 5000 is the
278    /// interesting size: two bytes under both, with different bits.
279    #[test]
280    fn varint_len_and_round_trip_follow_the_encoding() {
281        let mut buf = Vec::new();
282        DraftVersion::Draft14.encode_varint(VarInt::from_usize(5000), &mut buf);
283        assert_eq!(buf, vec![0x53, 0x88]);
284        assert_eq!(DraftVersion::Draft14.varint_len(buf[0]), 2);
285
286        let mut buf = Vec::new();
287        DraftVersion::Draft19.encode_varint(VarInt::from_usize(5000), &mut buf);
288        assert_eq!(buf, vec![0x93, 0x88]);
289        assert_eq!(DraftVersion::Draft19.varint_len(buf[0]), 2);
290
291        // 0x40 is a two-byte prefix under RFC 9000 and the one-byte value 64
292        // from draft-17 on.
293        assert_eq!(DraftVersion::Draft14.varint_len(0x40), 2);
294        assert_eq!(DraftVersion::Draft19.varint_len(0x40), 1);
295    }
296
297    #[test]
298    fn from_alpn_resolves_drafts_15_plus() {
299        assert_eq!(DraftVersion::from_alpn(b"moqt-15"), Some(DraftVersion::Draft15));
300        assert_eq!(DraftVersion::from_alpn(b"moqt-16"), Some(DraftVersion::Draft16));
301        assert_eq!(DraftVersion::from_alpn(b"moqt-17"), Some(DraftVersion::Draft17));
302        assert_eq!(DraftVersion::from_alpn(b"moqt-18"), Some(DraftVersion::Draft18));
303        assert_eq!(DraftVersion::from_alpn(b"moqt-19"), Some(DraftVersion::Draft19));
304    }
305
306    #[test]
307    fn from_alpn_none_for_moq_00_and_unknown() {
308        assert_eq!(DraftVersion::from_alpn(b"moq-00"), None);
309        assert_eq!(DraftVersion::from_alpn(b"h3"), None);
310        assert_eq!(DraftVersion::from_alpn(b""), None);
311        assert_eq!(DraftVersion::from_alpn(b"moqt-99"), None);
312    }
313
314    #[test]
315    fn from_alpn_round_trips_with_quic_alpn() {
316        for d in [
317            DraftVersion::Draft15,
318            DraftVersion::Draft16,
319            DraftVersion::Draft17,
320            DraftVersion::Draft18,
321            DraftVersion::Draft19,
322        ] {
323            assert_eq!(DraftVersion::from_alpn(d.quic_alpn()), Some(d));
324        }
325    }
326
327    #[test]
328    fn from_number_resolves_supported_range() {
329        for n in 7..=19u8 {
330            assert!(DraftVersion::from_number(n).is_some(), "draft {n} should resolve");
331        }
332    }
333
334    #[test]
335    fn from_number_none_outside_range() {
336        assert_eq!(DraftVersion::from_number(0), None);
337        assert_eq!(DraftVersion::from_number(6), None);
338        assert_eq!(DraftVersion::from_number(20), None);
339        assert_eq!(DraftVersion::from_number(255), None);
340    }
341}