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