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}