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}