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