moqtap_codec/message_names.rs
1//! The name a draft gives a control message type ID.
2//!
3//! A recorded trace stores a control message as a wire type ID and the draft it
4//! was read under — `mt` and the header's `protocol: moq-transport-NN` in a
5//! `.moqtrace` file. Both halves are needed to name it, because the ids are
6//! reused rather than retired: 0x07 is ANNOUNCE_OK through draft-13,
7//! PUBLISH_NAMESPACE_OK on draft-14 and REQUEST_OK from draft-15 on, and 0x0E
8//! moves TRACK_STATUS → TRACK_STATUS_OK → NAMESPACE_DONE across the same range.
9//! A table keyed on the id alone can only hedge — `SUBSCRIBE_DONE/PUBLISH_DONE`
10//! — and the hedge holds exactly while a rename keeps the number, which is not
11//! what happened to 0x07, 0x08, 0x0E or 0x11.
12//!
13//! So the lookup is per draft, and the per-draft half of it lives on each
14//! draft's own `MessageType::name`. Nothing here derives one draft's names from
15//! another's. [`message_type_name`] is re-exported at the crate root.
16//!
17//! # The names are the corpus's
18//!
19//! Each `name()` answers with the `message_type` field of that draft's
20//! `transport/draftNN/codec/messages/*.json` vectors — `subscribe`,
21//! `publish_namespace`, `goaway` — which is the same string the JavaScript
22//! codec's `MESSAGE_TYPE_MAP` answers with for the same id. That shared
23//! spelling is the point: a trace named by either implementation reads the same
24//! way, and `tests/message_type_names.rs` compares all fourteen drafts against
25//! the corpus in both directions so the two tables cannot drift apart quietly.
26//!
27//! The corpus is test-only — `Cargo.toml` excludes it from the package — so
28//! nothing here reads it at runtime. The names are transcribed into the draft
29//! modules and the test is what holds the transcription honest.
30
31use crate::version::DraftVersion;
32
33/// The name draft `draft` gives control message type `id`, or `None` if that
34/// draft assigns the id nothing.
35///
36/// `draft` is the draft number as the IETF writes it — 7 through 20 — which is
37/// what a trace header's `moq-transport-NN` carries and what
38/// [`DraftVersion::number`] returns. A number outside the range this crate
39/// implements answers `None`, as does an id the named draft leaves unassigned,
40/// and so does a draft whose feature flag is off in this build.
41///
42/// ```
43/// use moqtap_codec::message_type_name;
44///
45/// // A draft left out of the build answers `None` for every id, which is the
46/// // documented behaviour and also indistinguishable from a wrong table — so
47/// // the per-draft half of this example runs only where all of the drafts it
48/// // names are compiled in. The default feature set is `all-drafts`.
49/// # #[cfg(all(
50/// # feature = "draft07",
51/// # feature = "draft14",
52/// # feature = "draft16",
53/// # feature = "draft17",
54/// # feature = "draft19",
55/// # feature = "draft20"
56/// # ))]
57/// # {
58/// // 0x07 is three different messages across the range.
59/// assert_eq!(message_type_name(7, 0x07), Some("announce_ok"));
60/// assert_eq!(message_type_name(14, 0x07), Some("publish_namespace_ok"));
61/// assert_eq!(message_type_name(19, 0x07), Some("request_ok"));
62/// // 0x22 is PUBLISH_STATE_NOTIFY, and draft-20 is the first to assign it.
63/// assert_eq!(message_type_name(19, 0x22), None);
64/// assert_eq!(message_type_name(20, 0x22), Some("publish_state_notify"));
65///
66/// // The unified SETUP exists from draft-17 and nowhere before it.
67/// assert_eq!(message_type_name(16, 0x2F00), None);
68/// assert_eq!(message_type_name(17, 0x2F00), Some("setup"));
69/// # }
70///
71/// // A draft number outside the range answers `None` in every build.
72/// assert_eq!(message_type_name(6, 0x03), None);
73/// ```
74#[allow(unused_variables)]
75pub fn message_type_name(draft: u8, id: u64) -> Option<&'static str> {
76 match DraftVersion::from_number(draft)? {
77 #[cfg(feature = "draft07")]
78 DraftVersion::Draft07 => {
79 crate::draft07::message::MessageType::from_id(id).map(|t| t.name())
80 }
81 #[cfg(feature = "draft08")]
82 DraftVersion::Draft08 => {
83 crate::draft08::message::MessageType::from_id(id).map(|t| t.name())
84 }
85 #[cfg(feature = "draft09")]
86 DraftVersion::Draft09 => {
87 crate::draft09::message::MessageType::from_id(id).map(|t| t.name())
88 }
89 #[cfg(feature = "draft10")]
90 DraftVersion::Draft10 => {
91 crate::draft10::message::MessageType::from_id(id).map(|t| t.name())
92 }
93 #[cfg(feature = "draft11")]
94 DraftVersion::Draft11 => {
95 crate::draft11::message::MessageType::from_id(id).map(|t| t.name())
96 }
97 #[cfg(feature = "draft12")]
98 DraftVersion::Draft12 => {
99 crate::draft12::message::MessageType::from_id(id).map(|t| t.name())
100 }
101 #[cfg(feature = "draft13")]
102 DraftVersion::Draft13 => {
103 crate::draft13::message::MessageType::from_id(id).map(|t| t.name())
104 }
105 #[cfg(feature = "draft14")]
106 DraftVersion::Draft14 => {
107 crate::draft14::message::MessageType::from_id(id).map(|t| t.name())
108 }
109 #[cfg(feature = "draft15")]
110 DraftVersion::Draft15 => {
111 crate::draft15::message::MessageType::from_id(id).map(|t| t.name())
112 }
113 #[cfg(feature = "draft16")]
114 DraftVersion::Draft16 => {
115 crate::draft16::message::MessageType::from_id(id).map(|t| t.name())
116 }
117 #[cfg(feature = "draft17")]
118 DraftVersion::Draft17 => {
119 crate::draft17::message::MessageType::from_id(id).map(|t| t.name())
120 }
121 #[cfg(feature = "draft18")]
122 DraftVersion::Draft18 => {
123 crate::draft18::message::MessageType::from_id(id).map(|t| t.name())
124 }
125 #[cfg(feature = "draft19")]
126 DraftVersion::Draft19 => {
127 crate::draft19::message::MessageType::from_id(id).map(|t| t.name())
128 }
129 #[cfg(feature = "draft20")]
130 DraftVersion::Draft20 => {
131 crate::draft20::message::MessageType::from_id(id).map(|t| t.name())
132 }
133 // A draft this build did not enable. The number is one this crate
134 // implements, so it is not `from_number`'s `None`, and the honest answer
135 // is still that no name is available here.
136 #[allow(unreachable_patterns)]
137 _ => None,
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 /// The ids that moved, stated as the sequence they moved through. The
146 /// corpus sweep in `tests/message_type_names.rs` is what checks every id on
147 /// every draft; this is the handful that a draft-blind table gets wrong,
148 /// written out so the reason this function takes a draft is visible in the
149 /// crate itself.
150 ///
151 /// Gated on the three drafts it names. [`message_type_name`] answers `None`
152 /// for a draft no feature flag compiled in, which is the right answer and
153 /// not one this test can tell from a wrong table — so under a feature set
154 /// missing any of the three it would fail for a reason that has nothing to
155 /// do with the ids.
156 #[cfg(all(feature = "draft07", feature = "draft14", feature = "draft20"))]
157 #[test]
158 fn reused_ids_answer_per_draft() {
159 let reused: [(u64, [(u8, &str); 3]); 4] = [
160 (0x07, [(7, "announce_ok"), (14, "publish_namespace_ok"), (20, "request_ok")]),
161 (0x08, [(7, "announce_error"), (14, "publish_namespace_error"), (20, "namespace")]),
162 (0x0E, [(7, "track_status"), (14, "track_status_ok"), (20, "namespace_done")]),
163 (0x0B, [(7, "subscribe_done"), (14, "publish_done"), (20, "publish_done")]),
164 ];
165 for (id, expected) in reused {
166 for (draft, name) in expected {
167 assert_eq!(message_type_name(draft, id), Some(name), "draft-{draft} {id:#x}");
168 }
169 }
170 }
171
172 #[test]
173 fn none_outside_the_implemented_drafts() {
174 for draft in [0u8, 6, 21, 255] {
175 assert_eq!(message_type_name(draft, 0x03), None, "draft {draft}");
176 }
177 }
178
179 /// An id no draft in the range assigns. 0x3F is what the corpus's
180 /// `unknown-type.json` vectors put on the wire for exactly this.
181 ///
182 /// Needs no feature gate: a draft that is not compiled in answers `None`
183 /// for every id, which is what this asserts anyway.
184 #[test]
185 fn none_for_an_unassigned_id() {
186 for draft in 7..=20u8 {
187 assert_eq!(message_type_name(draft, 0x3F), None, "draft-{draft}");
188 }
189 }
190}