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    /// draft-ietf-moq-transport-21.
57    Draft21,
58    /// draft-ietf-moq-transport-22.
59    Draft22,
60    /// draft-ietf-moq-transport-23.
61    Draft23,
62}
63
64impl DraftVersion {
65    /// Every draft of the series, oldest first.
66    ///
67    /// The variants are not feature-gated, so this is the whole series whatever
68    /// the build compiles. It answers *which drafts exist*, which is a property
69    /// of the specification; *which drafts this binary can decode* is a
70    /// different question with a different answer per feature set, and
71    /// `moqtap-proxy`'s `draft_is_compiled` is where that one lives.
72    ///
73    /// Sweeps should read this rather than write their own list. A written-out
74    /// draft list is the most expensive silent defect this workspace has: it
75    /// compiles, it passes, and it tests one draft fewer than it claims to.
76    /// `shape/matcher.rs` shipped a `[DraftVersion; 13]` under a doc comment
77    /// claiming the whole series, and every unit test that swept it stopped
78    /// covering the newest draft without failing. A sweep over `ALL` cannot do
79    /// that, and a sweep that deliberately covers *less* than the series -- one
80    /// draft per era, or the drafts that have a joining fetch - still writes
81    /// its own list and still says why.
82    ///
83    /// An array rather than a slice, so `for d in DraftVersion::ALL` yields
84    /// drafts and not references and a caller can name the type. The length
85    /// beside it is checked by the compiler against the contents, so it cannot
86    /// silently disagree with them - and the contents are the half that goes
87    /// wrong. `scripts/check-draft-parity.py` holds those against the enum, the
88    /// per-draft source directories, the cargo features and the CI rows on
89    /// every run, and the tests below hold them against `from_number`.
90    pub const ALL: [DraftVersion; 17] = [
91        DraftVersion::Draft07,
92        DraftVersion::Draft08,
93        DraftVersion::Draft09,
94        DraftVersion::Draft10,
95        DraftVersion::Draft11,
96        DraftVersion::Draft12,
97        DraftVersion::Draft13,
98        DraftVersion::Draft14,
99        DraftVersion::Draft15,
100        DraftVersion::Draft16,
101        DraftVersion::Draft17,
102        DraftVersion::Draft18,
103        DraftVersion::Draft19,
104        DraftVersion::Draft20,
105        DraftVersion::Draft21,
106        DraftVersion::Draft22,
107        DraftVersion::Draft23,
108    ];
109
110    /// The newest draft of the series.
111    ///
112    /// `ALL` is ordered, so this is its last element. Written as a method
113    /// rather than left to the caller because `ALL.last().unwrap()` in a
114    /// hundred places is a hundred unwraps, and because a caller that wants
115    /// "the newest" almost always wants it infallibly.
116    pub const fn newest() -> DraftVersion {
117        // `ALL` is never empty, and a `const fn` cannot unwrap an `Option`, so
118        // the index is written out. If `ALL` ever became empty this would fail
119        // to compile rather than panic at run time.
120        DraftVersion::ALL[DraftVersion::ALL.len() - 1]
121    }
122
123    /// The MoQT version number this draft would announce in CLIENT_SETUP.
124    ///
125    /// Format: `0xff000000 + draft_number`.
126    ///
127    /// **From draft-15 on there is no such value on the wire at all.** Draft-15
128    /// deleted the version field from CLIENT_SETUP and moved version selection
129    /// into the ALPN (`moqt-<N>`, see [`Self::quic_alpn`]), so the number this
130    /// returns for drafts 15 through 23 — `0xff00000f` through `0xff000017` —
131    /// is a continuation of the mapping and not something a peer can observe or
132    /// send. Nothing in this crate encodes it for those drafts. It is kept so
133    /// that a caller with a draft in hand can name the version the series would
134    /// have used, and so the mapping does not acquire a hole.
135    ///
136    /// A tool that tries to detect the negotiated draft by looking for
137    /// `0xff0000NN` in a capture will find nothing from draft-15 on; the ALPN is
138    /// the only signal.
139    pub fn version_varint(&self) -> VarInt {
140        let n = match self {
141            DraftVersion::Draft07 => 7,
142            DraftVersion::Draft08 => 8,
143            DraftVersion::Draft09 => 9,
144            DraftVersion::Draft10 => 10,
145            DraftVersion::Draft11 => 11,
146            DraftVersion::Draft12 => 12,
147            DraftVersion::Draft13 => 13,
148            DraftVersion::Draft14 => 14,
149            DraftVersion::Draft15 => 15,
150            DraftVersion::Draft16 => 16,
151            DraftVersion::Draft17 => 17,
152            DraftVersion::Draft18 => 18,
153            DraftVersion::Draft19 => 19,
154            DraftVersion::Draft20 => 20,
155            DraftVersion::Draft21 => 21,
156            DraftVersion::Draft22 => 22,
157            DraftVersion::Draft23 => 23,
158        };
159        VarInt::from_usize(0xff000000 + n as usize)
160    }
161
162    /// The ALPN protocol identifier for raw QUIC connections.
163    ///
164    /// Drafts 07–14 all use `moq-00` and negotiate the draft version in
165    /// CLIENT_SETUP / SERVER_SETUP. Draft-15+ encode the draft number in the
166    /// ALPN itself (`moqt-<N>`), so version selection happens during the TLS
167    /// handshake rather than after it.
168    pub fn quic_alpn(&self) -> &'static [u8] {
169        match self {
170            DraftVersion::Draft07
171            | DraftVersion::Draft08
172            | DraftVersion::Draft09
173            | DraftVersion::Draft10
174            | DraftVersion::Draft11
175            | DraftVersion::Draft12
176            | DraftVersion::Draft13
177            | DraftVersion::Draft14 => b"moq-00",
178            DraftVersion::Draft15 => b"moqt-15",
179            DraftVersion::Draft16 => b"moqt-16",
180            DraftVersion::Draft17 => b"moqt-17",
181            DraftVersion::Draft18 => b"moqt-18",
182            DraftVersion::Draft19 => b"moqt-19",
183            DraftVersion::Draft20 => b"moqt-20",
184            DraftVersion::Draft21 => b"moqt-21",
185            DraftVersion::Draft22 => b"moqt-22",
186            DraftVersion::Draft23 => b"moqt-23",
187        }
188    }
189
190    /// Resolve an ALPN identifier to a specific draft version.
191    ///
192    /// Returns `Some` for ALPNs that unambiguously identify a draft
193    /// (`moqt-15` through `moqt-23`). Returns `None`
194    /// for `moq-00` — which covers drafts 07–14 and requires inspecting
195    /// CLIENT_SETUP's supported-versions list — and for any unrecognized
196    /// ALPN.
197    pub fn from_alpn(alpn: &[u8]) -> Option<DraftVersion> {
198        match alpn {
199            b"moqt-15" => Some(DraftVersion::Draft15),
200            b"moqt-16" => Some(DraftVersion::Draft16),
201            b"moqt-17" => Some(DraftVersion::Draft17),
202            b"moqt-18" => Some(DraftVersion::Draft18),
203            b"moqt-19" => Some(DraftVersion::Draft19),
204            b"moqt-20" => Some(DraftVersion::Draft20),
205            b"moqt-21" => Some(DraftVersion::Draft21),
206            b"moqt-22" => Some(DraftVersion::Draft22),
207            b"moqt-23" => Some(DraftVersion::Draft23),
208            _ => None,
209        }
210    }
211
212    /// Resolve a draft number (e.g. 7..=23) to a `DraftVersion`.
213    ///
214    /// Returns `None` for numbers outside the supported range.
215    pub fn from_number(n: u8) -> Option<DraftVersion> {
216        match n {
217            7 => Some(DraftVersion::Draft07),
218            8 => Some(DraftVersion::Draft08),
219            9 => Some(DraftVersion::Draft09),
220            10 => Some(DraftVersion::Draft10),
221            11 => Some(DraftVersion::Draft11),
222            12 => Some(DraftVersion::Draft12),
223            13 => Some(DraftVersion::Draft13),
224            14 => Some(DraftVersion::Draft14),
225            15 => Some(DraftVersion::Draft15),
226            16 => Some(DraftVersion::Draft16),
227            17 => Some(DraftVersion::Draft17),
228            18 => Some(DraftVersion::Draft18),
229            19 => Some(DraftVersion::Draft19),
230            20 => Some(DraftVersion::Draft20),
231            21 => Some(DraftVersion::Draft21),
232            22 => Some(DraftVersion::Draft22),
233            23 => Some(DraftVersion::Draft23),
234            _ => None,
235        }
236    }
237
238    /// Whether this draft uses a 16-bit big-endian message length in control
239    /// message framing (`true`) or a variable-length integer (`false`).
240    ///
241    /// Drafts 07-10 write `Length (i)`, drafts 11-22 `Length (16)`, and
242    /// draft-23 Section 9, Figure 6 writes "Message Length (vi64)".
243    pub fn uses_fixed_length_framing(&self) -> bool {
244        match self {
245            DraftVersion::Draft07
246            | DraftVersion::Draft08
247            | DraftVersion::Draft09
248            | DraftVersion::Draft10
249            | DraftVersion::Draft23 => false,
250            DraftVersion::Draft11
251            | DraftVersion::Draft12
252            | DraftVersion::Draft13
253            | DraftVersion::Draft14
254            | DraftVersion::Draft15
255            | DraftVersion::Draft16
256            | DraftVersion::Draft17
257            | DraftVersion::Draft18
258            | DraftVersion::Draft19
259            | DraftVersion::Draft20
260            | DraftVersion::Draft21
261            | DraftVersion::Draft22 => true,
262        }
263    }
264
265    /// Which variable-length integer encoding this draft's wire format uses.
266    ///
267    /// Matched draft by draft rather than derived from the number. The series
268    /// has already changed encoding once mid-stream and revised it again a
269    /// draft later, so there is no rule to extrapolate from: adding a variant
270    /// to [`DraftVersion`] must fail to compile here until someone reads that
271    /// draft and says which encoding it uses.
272    pub fn varint_encoding(&self) -> VarIntEncoding {
273        match self {
274            DraftVersion::Draft07
275            | DraftVersion::Draft08
276            | DraftVersion::Draft09
277            | DraftVersion::Draft10
278            | DraftVersion::Draft11
279            | DraftVersion::Draft12
280            | DraftVersion::Draft13
281            | DraftVersion::Draft14
282            | DraftVersion::Draft15
283            | DraftVersion::Draft16 => VarIntEncoding::Rfc9000,
284            DraftVersion::Draft17 => VarIntEncoding::Moqt17,
285            // Draft-20 Section 1.4.1 is draft-18's encoding verbatim: the same
286            // leading-ones-count prefix over all nine lengths. The revision
287            // changed the hyphen in "Variable-length" in the heading and
288            // nothing else about it. Drafts 21 through 23 state it in Section
289            // 8.1 and leave the integer alone.
290            DraftVersion::Draft18
291            | DraftVersion::Draft19
292            | DraftVersion::Draft20
293            | DraftVersion::Draft21
294            | DraftVersion::Draft22
295            | DraftVersion::Draft23 => VarIntEncoding::Moqt18,
296        }
297    }
298
299    /// Whether this draft uses one of MoQT's own variable-length integers
300    /// rather than RFC 9000's.
301    pub fn uses_moqt_varint(&self) -> bool {
302        self.varint_encoding() != VarIntEncoding::Rfc9000
303    }
304
305    /// The total encoded length of a variable-length integer, from its first
306    /// byte, under this draft's encoding.
307    ///
308    /// Available without a buffer, because a reader needs it to know how many
309    /// bytes to wait for before it can decode at all. On draft-17 a first byte
310    /// of `11111100` reports 7 even though the draft forbids that length: the
311    /// reader waits for the whole field, then [`Self::decode_varint`] rejects
312    /// it.
313    pub fn varint_len(&self, first_byte: u8) -> usize {
314        match self.varint_encoding() {
315            VarIntEncoding::Rfc9000 => 1 << (first_byte >> 6),
316            VarIntEncoding::Moqt17 | VarIntEncoding::Moqt18 => {
317                if first_byte == 0xFF {
318                    9
319                } else {
320                    first_byte.leading_ones() as usize + 1
321                }
322            }
323        }
324    }
325
326    /// Decode a variable-length integer under this draft's encoding.
327    pub fn decode_varint(&self, buf: &mut impl Buf) -> Result<VarInt, VarIntError> {
328        match self.varint_encoding() {
329            VarIntEncoding::Rfc9000 => VarInt::decode(buf),
330            VarIntEncoding::Moqt17 => VarInt::decode_moqt::<Moqt17>(buf),
331            VarIntEncoding::Moqt18 => VarInt::decode_moqt::<Moqt18>(buf),
332        }
333    }
334
335    /// Encode a variable-length integer under this draft's encoding.
336    pub fn encode_varint(&self, value: VarInt, buf: &mut impl BufMut) {
337        match self.varint_encoding() {
338            VarIntEncoding::Rfc9000 => value.encode(buf),
339            VarIntEncoding::Moqt17 => value.encode_moqt::<Moqt17>(buf),
340            VarIntEncoding::Moqt18 => value.encode_moqt::<Moqt18>(buf),
341        }
342    }
343
344    /// The draft number (e.g. 7, 14, 23).
345    pub fn number(&self) -> u8 {
346        match self {
347            DraftVersion::Draft07 => 7,
348            DraftVersion::Draft08 => 8,
349            DraftVersion::Draft09 => 9,
350            DraftVersion::Draft10 => 10,
351            DraftVersion::Draft11 => 11,
352            DraftVersion::Draft12 => 12,
353            DraftVersion::Draft13 => 13,
354            DraftVersion::Draft14 => 14,
355            DraftVersion::Draft15 => 15,
356            DraftVersion::Draft16 => 16,
357            DraftVersion::Draft17 => 17,
358            DraftVersion::Draft18 => 18,
359            DraftVersion::Draft19 => 19,
360            DraftVersion::Draft20 => 20,
361            DraftVersion::Draft21 => 21,
362            DraftVersion::Draft22 => 22,
363            DraftVersion::Draft23 => 23,
364        }
365    }
366}
367
368impl std::fmt::Display for DraftVersion {
369    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370        write!(f, "draft-{:02}", self.number())
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    /// `ALL` and `from_number` are two statements of the same set.
379    ///
380    /// Neither is derived from the other - one is a list of variants and the
381    /// other a `match` over numbers - so they can disagree, and this is what
382    /// says so. Adding a variant makes `number()` stop compiling, which is how
383    /// the enum forces the first edit; this is what forces the rest.
384    #[test]
385    fn all_and_from_number_agree_on_which_drafts_exist() {
386        let derived: Vec<DraftVersion> =
387            (0u8..=255).filter_map(DraftVersion::from_number).collect();
388        assert_eq!(
389            DraftVersion::ALL.as_slice(),
390            derived.as_slice(),
391            "`DraftVersion::ALL` and `from_number` disagree about which drafts exist"
392        );
393    }
394
395    /// Oldest first, with no gaps.
396    ///
397    /// Both halves are load-bearing for callers: the order is what makes
398    /// `ALL.last()` the newest draft and `newest()` meaningful, and the
399    /// contiguity is what lets a sweep say "every draft from N on" as a slice
400    /// of `ALL` rather than a second list.
401    #[test]
402    fn all_is_ordered_and_contiguous() {
403        for pair in DraftVersion::ALL.windows(2) {
404            assert_eq!(
405                pair[1].number(),
406                pair[0].number() + 1,
407                "`ALL` jumps from draft-{:02} to draft-{:02}",
408                pair[0].number(),
409                pair[1].number()
410            );
411        }
412    }
413
414    /// `newest()` is the last of `ALL`, and nothing is newer.
415    #[test]
416    fn newest_is_the_end_of_the_series() {
417        assert_eq!(Some(DraftVersion::newest()), DraftVersion::ALL.last().copied());
418        assert_eq!(
419            DraftVersion::from_number(DraftVersion::newest().number() + 1),
420            None,
421            "a draft past the newest resolves, so `ALL` is short"
422        );
423    }
424
425    /// The draft-to-encoding map, stated once so a change to it is a change to
426    /// this list rather than a silent consequence of a comparison.
427    #[test]
428    fn every_draft_states_its_varint_encoding() {
429        use VarIntEncoding::*;
430        let expected = [
431            (DraftVersion::Draft07, Rfc9000),
432            (DraftVersion::Draft08, Rfc9000),
433            (DraftVersion::Draft09, Rfc9000),
434            (DraftVersion::Draft10, Rfc9000),
435            (DraftVersion::Draft11, Rfc9000),
436            (DraftVersion::Draft12, Rfc9000),
437            (DraftVersion::Draft13, Rfc9000),
438            (DraftVersion::Draft14, Rfc9000),
439            (DraftVersion::Draft15, Rfc9000),
440            (DraftVersion::Draft16, Rfc9000),
441            (DraftVersion::Draft17, Moqt17),
442            (DraftVersion::Draft18, Moqt18),
443            (DraftVersion::Draft19, Moqt18),
444            (DraftVersion::Draft20, Moqt18),
445            (DraftVersion::Draft21, Moqt18),
446            (DraftVersion::Draft22, Moqt18),
447            (DraftVersion::Draft23, Moqt18),
448        ];
449        for (draft, encoding) in expected {
450            assert_eq!(draft.varint_encoding(), encoding, "{draft}");
451            assert_eq!(draft.uses_moqt_varint(), encoding != Rfc9000, "{draft}");
452        }
453    }
454
455    /// The same value, in the encoding each era actually uses. 5000 is the
456    /// interesting size: two bytes under both, with different bits.
457    #[test]
458    fn varint_len_and_round_trip_follow_the_encoding() {
459        let mut buf = Vec::new();
460        DraftVersion::Draft14.encode_varint(VarInt::from_usize(5000), &mut buf);
461        assert_eq!(buf, vec![0x53, 0x88]);
462        assert_eq!(DraftVersion::Draft14.varint_len(buf[0]), 2);
463
464        let mut buf = Vec::new();
465        DraftVersion::Draft20.encode_varint(VarInt::from_usize(5000), &mut buf);
466        assert_eq!(buf, vec![0x93, 0x88]);
467        assert_eq!(DraftVersion::Draft20.varint_len(buf[0]), 2);
468
469        // 0x40 is a two-byte prefix under RFC 9000 and the one-byte value 64
470        // from draft-17 on.
471        assert_eq!(DraftVersion::Draft14.varint_len(0x40), 2);
472        assert_eq!(DraftVersion::Draft20.varint_len(0x40), 1);
473    }
474
475    #[test]
476    fn from_alpn_resolves_drafts_15_plus() {
477        assert_eq!(DraftVersion::from_alpn(b"moqt-15"), Some(DraftVersion::Draft15));
478        assert_eq!(DraftVersion::from_alpn(b"moqt-16"), Some(DraftVersion::Draft16));
479        assert_eq!(DraftVersion::from_alpn(b"moqt-17"), Some(DraftVersion::Draft17));
480        assert_eq!(DraftVersion::from_alpn(b"moqt-18"), Some(DraftVersion::Draft18));
481        assert_eq!(DraftVersion::from_alpn(b"moqt-19"), Some(DraftVersion::Draft19));
482        assert_eq!(DraftVersion::from_alpn(b"moqt-20"), Some(DraftVersion::Draft20));
483        assert_eq!(DraftVersion::from_alpn(b"moqt-21"), Some(DraftVersion::Draft21));
484        assert_eq!(DraftVersion::from_alpn(b"moqt-22"), Some(DraftVersion::Draft22));
485        assert_eq!(DraftVersion::from_alpn(b"moqt-23"), Some(DraftVersion::Draft23));
486    }
487
488    #[test]
489    fn from_alpn_none_for_moq_00_and_unknown() {
490        assert_eq!(DraftVersion::from_alpn(b"moq-00"), None);
491        assert_eq!(DraftVersion::from_alpn(b"h3"), None);
492        assert_eq!(DraftVersion::from_alpn(b""), None);
493        assert_eq!(DraftVersion::from_alpn(b"moqt-99"), None);
494    }
495
496    #[test]
497    fn from_alpn_round_trips_with_quic_alpn() {
498        // Every draft with an ALPN of its own, off `ALL` rather than
499        // listed: the cohort is "draft-15 onwards", and a list would have
500        // to be extended by hand for each new draft to keep covering it.
501        for d in DraftVersion::ALL.iter().copied().filter(|d| d.number() >= 15) {
502            assert_eq!(DraftVersion::from_alpn(d.quic_alpn()), Some(d));
503        }
504    }
505
506    #[test]
507    fn from_number_resolves_supported_range() {
508        for d in DraftVersion::ALL {
509            assert_eq!(
510                DraftVersion::from_number(d.number()),
511                Some(d),
512                "draft {d:?} should resolve"
513            );
514        }
515    }
516
517    #[test]
518    fn from_number_none_outside_range() {
519        assert_eq!(DraftVersion::from_number(0), None);
520        assert_eq!(DraftVersion::from_number(6), None);
521        let newest = DraftVersion::ALL.last().unwrap().number();
522        assert_eq!(DraftVersion::from_number(newest + 1), None);
523        assert_eq!(DraftVersion::from_number(255), None);
524    }
525}