Skip to main content

moq_net/
version.rs

1use std::fmt;
2use std::str::FromStr;
3
4use crate::{coding, ietf, lite};
5
6/// The versions of MoQ that are negotiated via SETUP.
7///
8/// Ordered by preference, with the client's preference taking priority.
9/// This intentionally includes only SETUP-negotiated versions (Lite02, Lite01, Draft14);
10/// Lite03 and newer IETF drafts negotiate via dedicated ALPNs instead.
11pub(crate) const NEGOTIATED: [Version; 3] = [
12	Version::Lite(lite::Version::Lite02),
13	Version::Lite(lite::Version::Lite01),
14	Version::Ietf(ietf::Version::Draft14),
15];
16
17/// ALPN strings for supported versions, most-preferred first. `ALPNS[0]` is the
18/// newest moq-lite ALPN that both sides converge on.
19///
20/// `ALPN_LITE_07_WIP` is deliberately absent: lite-07's wire format is still
21/// work-in-progress, so it is never advertised or negotiated by default. It is only
22/// reachable when both peers explicitly opt in (e.g. `--version moq-lite-07-wip`).
23pub const ALPNS: &[&str] = &[
24	ALPN_LITE_06,
25	ALPN_LITE_05,
26	ALPN_LITE_04,
27	ALPN_LITE_03,
28	ALPN_LITE,
29	ALPN_22,
30	ALPN_21,
31	ALPN_20,
32	ALPN_19,
33	ALPN_18,
34	ALPN_17,
35	ALPN_16,
36	ALPN_15,
37	ALPN_14,
38];
39
40// ALPN constants
41pub(crate) const ALPN_LITE: &str = "moql";
42pub(crate) const ALPN_LITE_03: &str = "moq-lite-03";
43pub(crate) const ALPN_LITE_04: &str = "moq-lite-04";
44pub(crate) const ALPN_LITE_05: &str = "moq-lite-05";
45pub(crate) const ALPN_LITE_06: &str = "moq-lite-06";
46pub(crate) const ALPN_LITE_07_WIP: &str = "moq-lite-07-wip";
47pub(crate) const ALPN_14: &str = "moq-00";
48pub(crate) const ALPN_15: &str = "moqt-15";
49pub(crate) const ALPN_16: &str = "moqt-16";
50pub(crate) const ALPN_17: &str = "moqt-17";
51pub(crate) const ALPN_18: &str = "moqt-18";
52pub(crate) const ALPN_19: &str = "moqt-19";
53pub(crate) const ALPN_20: &str = "moqt-20";
54pub(crate) const ALPN_21: &str = "moqt-21";
55pub(crate) const ALPN_22: &str = "moqt-22";
56
57const ALL: [Version; 16] = [
58	Version::Lite(lite::Version::Lite07),
59	Version::Lite(lite::Version::Lite06),
60	Version::Lite(lite::Version::Lite05),
61	Version::Lite(lite::Version::Lite04),
62	Version::Lite(lite::Version::Lite03),
63	Version::Lite(lite::Version::Lite02),
64	Version::Lite(lite::Version::Lite01),
65	Version::Ietf(ietf::Version::Draft22),
66	Version::Ietf(ietf::Version::Draft21),
67	Version::Ietf(ietf::Version::Draft20),
68	Version::Ietf(ietf::Version::Draft19),
69	Version::Ietf(ietf::Version::Draft18),
70	Version::Ietf(ietf::Version::Draft17),
71	Version::Ietf(ietf::Version::Draft16),
72	Version::Ietf(ietf::Version::Draft15),
73	Version::Ietf(ietf::Version::Draft14),
74];
75
76/// A MoQ protocol version.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
78#[non_exhaustive]
79pub enum Version {
80	/// A `moq-lite` draft, the simplified protocol this project specifies.
81	Lite(lite::Version),
82	/// An IETF `moq-transport` draft.
83	Ietf(ietf::Version),
84}
85
86impl Version {
87	/// Iterate the names accepted by [`FromStr`].
88	pub fn names() -> impl Iterator<Item = &'static str> {
89		ALL.iter().map(Self::as_str)
90	}
91
92	/// Handshake / config name, e.g. `moq-lite-05` or `moq-transport-20`.
93	pub fn as_str(&self) -> &'static str {
94		match self {
95			Self::Lite(lite::Version::Lite01) => "moq-lite-01",
96			Self::Lite(lite::Version::Lite02) => "moq-lite-02",
97			Self::Lite(lite::Version::Lite03) => "moq-lite-03",
98			Self::Lite(lite::Version::Lite04) => "moq-lite-04",
99			Self::Lite(lite::Version::Lite05) => "moq-lite-05",
100			Self::Lite(lite::Version::Lite06) => "moq-lite-06",
101			Self::Lite(lite::Version::Lite07) => "moq-lite-07-wip",
102			Self::Ietf(ietf::Version::Draft14) => "moq-transport-14",
103			Self::Ietf(ietf::Version::Draft15) => "moq-transport-15",
104			Self::Ietf(ietf::Version::Draft16) => "moq-transport-16",
105			Self::Ietf(ietf::Version::Draft17) => "moq-transport-17",
106			Self::Ietf(ietf::Version::Draft18) => "moq-transport-18",
107			Self::Ietf(ietf::Version::Draft19) => "moq-transport-19",
108			Self::Ietf(ietf::Version::Draft20) => "moq-transport-20",
109			Self::Ietf(ietf::Version::Draft21) => "moq-transport-21",
110			Self::Ietf(ietf::Version::Draft22) => "moq-transport-22",
111		}
112	}
113
114	/// Parse from wire version code (used during SETUP negotiation).
115	pub fn from_code(code: u64) -> Option<Self> {
116		match code {
117			0xff0dad01 => Some(Self::Lite(lite::Version::Lite01)),
118			0xff0dad02 => Some(Self::Lite(lite::Version::Lite02)),
119			0xff0dad03 => Some(Self::Lite(lite::Version::Lite03)),
120			0xff0dad04 => Some(Self::Lite(lite::Version::Lite04)),
121			0xff0dad05 => Some(Self::Lite(lite::Version::Lite05)),
122			0xff0dad06 => Some(Self::Lite(lite::Version::Lite06)),
123			0xff0dad07 => Some(Self::Lite(lite::Version::Lite07)),
124			0xff00000e => Some(Self::Ietf(ietf::Version::Draft14)),
125			0xff00000f => Some(Self::Ietf(ietf::Version::Draft15)),
126			0xff000010 => Some(Self::Ietf(ietf::Version::Draft16)),
127			0xff000011 => Some(Self::Ietf(ietf::Version::Draft17)),
128			0xff000012 => Some(Self::Ietf(ietf::Version::Draft18)),
129			0xff000013 => Some(Self::Ietf(ietf::Version::Draft19)),
130			0xff000014 => Some(Self::Ietf(ietf::Version::Draft20)),
131			0xff000015 => Some(Self::Ietf(ietf::Version::Draft21)),
132			0xff000016 => Some(Self::Ietf(ietf::Version::Draft22)),
133			_ => None,
134		}
135	}
136
137	/// Get the wire version code.
138	pub fn code(&self) -> u64 {
139		match self {
140			Self::Lite(lite::Version::Lite01) => 0xff0dad01,
141			Self::Lite(lite::Version::Lite02) => 0xff0dad02,
142			Self::Lite(lite::Version::Lite03) => 0xff0dad03,
143			Self::Lite(lite::Version::Lite04) => 0xff0dad04,
144			Self::Lite(lite::Version::Lite05) => 0xff0dad05,
145			Self::Lite(lite::Version::Lite06) => 0xff0dad06,
146			Self::Lite(lite::Version::Lite07) => 0xff0dad07,
147			Self::Ietf(ietf::Version::Draft14) => 0xff00000e,
148			Self::Ietf(ietf::Version::Draft15) => 0xff00000f,
149			Self::Ietf(ietf::Version::Draft16) => 0xff000010,
150			Self::Ietf(ietf::Version::Draft17) => 0xff000011,
151			Self::Ietf(ietf::Version::Draft18) => 0xff000012,
152			Self::Ietf(ietf::Version::Draft19) => 0xff000013,
153			Self::Ietf(ietf::Version::Draft20) => 0xff000014,
154			Self::Ietf(ietf::Version::Draft21) => 0xff000015,
155			Self::Ietf(ietf::Version::Draft22) => 0xff000016,
156		}
157	}
158
159	/// Parse from ALPN string.
160	///
161	/// Returns `None` for `ALPN_LITE` since multiple versions share
162	/// that ALPN, requiring SETUP negotiation to determine the version.
163	pub fn from_alpn(alpn: &str) -> Option<Self> {
164		match alpn {
165			ALPN_LITE => None, // Multiple versions share this ALPN, need SETUP negotiation
166			ALPN_LITE_03 => Some(Self::Lite(lite::Version::Lite03)),
167			ALPN_LITE_04 => Some(Self::Lite(lite::Version::Lite04)),
168			ALPN_LITE_05 => Some(Self::Lite(lite::Version::Lite05)),
169			ALPN_LITE_06 => Some(Self::Lite(lite::Version::Lite06)),
170			ALPN_LITE_07_WIP => Some(Self::Lite(lite::Version::Lite07)),
171			ALPN_14 => Some(Self::Ietf(ietf::Version::Draft14)),
172			ALPN_15 => Some(Self::Ietf(ietf::Version::Draft15)),
173			ALPN_16 => Some(Self::Ietf(ietf::Version::Draft16)),
174			ALPN_17 => Some(Self::Ietf(ietf::Version::Draft17)),
175			ALPN_18 => Some(Self::Ietf(ietf::Version::Draft18)),
176			ALPN_19 => Some(Self::Ietf(ietf::Version::Draft19)),
177			ALPN_20 => Some(Self::Ietf(ietf::Version::Draft20)),
178			ALPN_21 => Some(Self::Ietf(ietf::Version::Draft21)),
179			ALPN_22 => Some(Self::Ietf(ietf::Version::Draft22)),
180			_ => None,
181		}
182	}
183
184	/// Returns the ALPN string for this version.
185	pub fn alpn(&self) -> &'static str {
186		match self {
187			Self::Lite(lite::Version::Lite07) => ALPN_LITE_07_WIP,
188			Self::Lite(lite::Version::Lite06) => ALPN_LITE_06,
189			Self::Lite(lite::Version::Lite05) => ALPN_LITE_05,
190			Self::Lite(lite::Version::Lite04) => ALPN_LITE_04,
191			Self::Lite(lite::Version::Lite03) => ALPN_LITE_03,
192			Self::Lite(lite::Version::Lite01 | lite::Version::Lite02) => ALPN_LITE,
193			Self::Ietf(ietf::Version::Draft14) => ALPN_14,
194			Self::Ietf(ietf::Version::Draft15) => ALPN_15,
195			Self::Ietf(ietf::Version::Draft16) => ALPN_16,
196			Self::Ietf(ietf::Version::Draft17) => ALPN_17,
197			Self::Ietf(ietf::Version::Draft18) => ALPN_18,
198			Self::Ietf(ietf::Version::Draft19) => ALPN_19,
199			Self::Ietf(ietf::Version::Draft20) => ALPN_20,
200			Self::Ietf(ietf::Version::Draft21) => ALPN_21,
201			Self::Ietf(ietf::Version::Draft22) => ALPN_22,
202		}
203	}
204
205	/// Whether this version uses SETUP version-code negotiation
206	/// (as opposed to ALPN-only).
207	pub fn uses_setup_negotiation(&self) -> bool {
208		matches!(
209			self,
210			Self::Lite(lite::Version::Lite01 | lite::Version::Lite02) | Self::Ietf(ietf::Version::Draft14)
211		)
212	}
213
214	/// Whether this is a lite protocol version.
215	pub fn is_lite(&self) -> bool {
216		match self {
217			Self::Lite(_) => true,
218			Self::Ietf(_) => false,
219		}
220	}
221
222	/// Whether this is an IETF protocol version.
223	pub fn is_ietf(&self) -> bool {
224		match self {
225			Self::Ietf(_) => true,
226			Self::Lite(_) => false,
227		}
228	}
229}
230
231impl fmt::Display for Version {
232	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233		f.write_str(self.as_str())
234	}
235}
236
237impl FromStr for Version {
238	type Err = String;
239
240	fn from_str(s: &str) -> Result<Self, Self::Err> {
241		ALL.iter()
242			.find(|version| version.as_str() == s)
243			.copied()
244			.ok_or_else(|| format!("unknown version: {s}"))
245	}
246}
247
248impl serde::Serialize for Version {
249	fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
250		serializer.serialize_str(&self.to_string())
251	}
252}
253
254impl<'de> serde::Deserialize<'de> for Version {
255	fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
256		let s = String::deserialize(deserializer)?;
257		s.parse().map_err(serde::de::Error::custom)
258	}
259}
260
261impl TryFrom<coding::Version> for Version {
262	type Error = ();
263
264	fn try_from(value: coding::Version) -> Result<Self, Self::Error> {
265		Self::from_code(value.0).ok_or(())
266	}
267}
268
269impl coding::Decode<Version> for Version {
270	fn decode<R: bytes::Buf>(r: &mut R, version: Version) -> Result<Self, coding::DecodeError> {
271		coding::Version::decode(r, version).and_then(|v| v.try_into().map_err(|_| coding::DecodeError::InvalidValue))
272	}
273}
274
275impl coding::Encode<Version> for Version {
276	fn encode<W: bytes::BufMut>(&self, w: &mut W, v: Version) -> Result<(), coding::EncodeError> {
277		coding::Version::from(*self).encode(w, v)
278	}
279}
280
281impl From<Version> for coding::Version {
282	fn from(value: Version) -> Self {
283		Self(value.code())
284	}
285}
286
287impl From<Vec<Version>> for coding::Versions {
288	fn from(value: Vec<Version>) -> Self {
289		let inner: Vec<coding::Version> = value.into_iter().map(|v| v.into()).collect();
290		coding::Versions::from(inner)
291	}
292}
293
294/// A set of supported MoQ versions.
295#[derive(Debug, Clone)]
296pub struct Versions(Vec<Version>);
297
298impl Versions {
299	/// All versions exposed by default.
300	///
301	/// `Lite07` is intentionally excluded: its wire format is still work-in-progress,
302	/// so it is not advertised until a caller opts in explicitly (e.g. a pinned
303	/// `version = ["moq-lite-07-wip"]`). An opt-in set that includes it negotiates normally.
304	pub fn all() -> Self {
305		Self(
306			ALL.iter()
307				.filter(|version| !matches!(version, Version::Lite(lite::Version::Lite07)))
308				.copied()
309				.collect(),
310		)
311	}
312
313	/// Compute the unique ALPN strings needed for these versions.
314	pub fn alpns(&self) -> Vec<&'static str> {
315		let mut alpns = Vec::new();
316		for v in &self.0 {
317			let alpn = v.alpn();
318			if !alpns.contains(&alpn) {
319				alpns.push(alpn);
320			}
321		}
322		alpns
323	}
324
325	/// Return only versions present in both self and other, or `None` if the intersection is empty.
326	pub fn filter(&self, other: &Versions) -> Option<Versions> {
327		let filtered: Vec<Version> = self.0.iter().filter(|v| other.0.contains(v)).copied().collect();
328		if filtered.is_empty() {
329			None
330		} else {
331			Some(Versions(filtered))
332		}
333	}
334
335	/// Check if a specific version is in this set.
336	pub fn select(&self, version: Version) -> Option<Version> {
337		self.0.contains(&version).then_some(version)
338	}
339
340	/// Returns `true` if the set includes this version.
341	pub fn contains(&self, version: &Version) -> bool {
342		self.0.contains(version)
343	}
344
345	/// Iterate the set in preference order, most preferred first.
346	pub fn iter(&self) -> impl Iterator<Item = &Version> {
347		self.0.iter()
348	}
349}
350
351impl Default for Versions {
352	fn default() -> Self {
353		Self::all()
354	}
355}
356
357impl From<Version> for Versions {
358	fn from(value: Version) -> Self {
359		Self(vec![value])
360	}
361}
362
363impl From<Vec<Version>> for Versions {
364	fn from(value: Vec<Version>) -> Self {
365		Self(value)
366	}
367}
368
369impl<const N: usize> From<[Version; N]> for Versions {
370	fn from(value: [Version; N]) -> Self {
371		Self(value.to_vec())
372	}
373}
374
375impl From<Versions> for coding::Versions {
376	fn from(value: Versions) -> Self {
377		let inner: Vec<coding::Version> = value.0.into_iter().map(|v| v.into()).collect();
378		coding::Versions::from(inner)
379	}
380}
381
382#[cfg(test)]
383mod tests {
384	use super::*;
385
386	#[test]
387	fn default_versions_prefer_lite_06() {
388		let newest = Version::Lite(lite::Version::Lite06);
389		assert_eq!(newest.alpn(), "moq-lite-06");
390		assert_eq!(Version::from_alpn("moq-lite-06-wip"), None);
391		assert!("moq-lite-06-wip".parse::<Version>().is_err());
392		assert_eq!(Versions::all().iter().next(), Some(&newest));
393		assert_eq!(Versions::all().alpns().first(), Some(&newest.alpn()));
394		assert_eq!(ALPNS.first(), Some(&newest.alpn()));
395	}
396
397	#[test]
398	fn lite_07_wip_is_opt_in_only() {
399		let wip = Version::Lite(lite::Version::Lite07);
400		assert_eq!(wip.alpn(), "moq-lite-07-wip");
401		assert_eq!(wip.to_string(), "moq-lite-07-wip");
402		assert_eq!("moq-lite-07-wip".parse::<Version>(), Ok(wip));
403		assert_eq!(Version::from_alpn("moq-lite-07-wip"), Some(wip));
404		assert!("moq-lite-07".parse::<Version>().is_err());
405		assert_eq!(Version::from_alpn("moq-lite-07"), None);
406
407		assert!(!Versions::all().contains(&wip));
408		assert!(!ALPNS.contains(&wip.alpn()));
409		assert_eq!(Versions::from(wip).alpns(), [wip.alpn()]);
410	}
411}