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 dispatch itself is `crate::draft_table::by_draft` — private, so not a
18//! link — which
19//! [`crate::setup_option_names`] uses for the other registry. The shared shape
20//! has no catch-all in it at all, so a draft nobody has added yet is a compile
21//! error here rather than the same quiet `None` a build that left a draft out
22//! gets; see that module for what that buys and what it costs.
23//!
24//! # A name is not a concept either
25//!
26//! The names are reused as well as the ids, and `track_status` swaps which side
27//! of the exchange it names at draft-13:
28//!
29//! | | Request | Response |
30//! |---|---|---|
31//! | **Drafts 07-12** | `track_status_request` (0x0D) | `track_status` (0x0E) |
32//! | **Drafts 13-20** | `track_status` (0x0D) | `track_status_ok` (0x0E) |
33//!
34//! Every answer is right about its own draft, so comparing two of them by name
35//! yields a wrong conclusion out of two correct lookups.
36//!
37//! # The names are the corpus's
38//!
39//! Each `name()` answers with the `message_type` field of that draft's
40//! `transport/draftNN/codec/messages/*.json` vectors — `subscribe`,
41//! `publish_namespace`, `goaway` — which is the same string the JavaScript
42//! codec's `MESSAGE_TYPE_MAP` answers with for the same id. That shared
43//! spelling is the point: a trace named by either implementation reads the same
44//! way, and `tests/message_type_names.rs` compares all fourteen drafts against
45//! the corpus in both directions so the two tables cannot drift apart quietly.
46//!
47//! The corpus is test-only — `Cargo.toml` excludes it from the package — so
48//! nothing here reads it at runtime. The names are transcribed into the draft
49//! modules and the test is what holds the transcription honest.
50
51/// The name draft `draft` gives control message type `id`, or `None` if that
52/// draft assigns the id nothing.
53///
54/// `draft` is the draft number as the IETF writes it — 7 through 20 — which is
55/// what a trace header's `moq-transport-NN` carries and what
56/// [`crate::version::DraftVersion::number`] returns. A number outside the range
57/// this crate
58/// implements answers `None`, as does an id the named draft leaves unassigned,
59/// and so does a draft whose feature flag is off in this build.
60///
61/// Two drafts' answers are not comparable just because they match:
62/// `message_type_name(11, 0x0E)` and `message_type_name(14, 0x0D)` both answer
63/// `track_status` and name opposite sides of an exchange. See the module docs.
64///
65/// ```
66/// use moqtap_codec::message_type_name;
67///
68/// // A draft left out of the build answers `None` for every id, which is the
69/// // documented behaviour and also indistinguishable from a wrong table — so
70/// // the per-draft half of this example runs only where all of the drafts it
71/// // names are compiled in. The default feature set is `all-drafts`.
72/// # #[cfg(all(
73/// # feature = "draft07",
74/// # feature = "draft14",
75/// # feature = "draft16",
76/// # feature = "draft17",
77/// # feature = "draft19",
78/// # feature = "draft20"
79/// # ))]
80/// # {
81/// // 0x07 is three different messages across the range.
82/// assert_eq!(message_type_name(7, 0x07), Some("announce_ok"));
83/// assert_eq!(message_type_name(14, 0x07), Some("publish_namespace_ok"));
84/// assert_eq!(message_type_name(19, 0x07), Some("request_ok"));
85/// // 0x22 is PUBLISH_STATE_NOTIFY, and draft-20 is the first to assign it.
86/// assert_eq!(message_type_name(19, 0x22), None);
87/// assert_eq!(message_type_name(20, 0x22), Some("publish_state_notify"));
88///
89/// // The unified SETUP exists from draft-17 and nowhere before it.
90/// assert_eq!(message_type_name(16, 0x2F00), None);
91/// assert_eq!(message_type_name(17, 0x2F00), Some("setup"));
92/// # }
93///
94/// // A draft number outside the range answers `None` in every build.
95/// assert_eq!(message_type_name(6, 0x03), None);
96/// ```
97// `id` is read by every arm in any build that has a draft, and by none in the
98// zero-draft build, where every arm takes its `#[cfg(not(feature = ...))]`
99// form. See `crate::draft_table` for why both forms are spelled out.
100#[allow(unused_variables)]
101pub fn message_type_name(draft: u8, id: u64) -> Option<&'static str> {
102 crate::draft_table::by_draft! { draft, None,
103 ("draft07", Draft07) => crate::draft07::message::MessageType::from_id(id).map(|t| t.name()),
104 ("draft08", Draft08) => crate::draft08::message::MessageType::from_id(id).map(|t| t.name()),
105 ("draft09", Draft09) => crate::draft09::message::MessageType::from_id(id).map(|t| t.name()),
106 ("draft10", Draft10) => crate::draft10::message::MessageType::from_id(id).map(|t| t.name()),
107 ("draft11", Draft11) => crate::draft11::message::MessageType::from_id(id).map(|t| t.name()),
108 ("draft12", Draft12) => crate::draft12::message::MessageType::from_id(id).map(|t| t.name()),
109 ("draft13", Draft13) => crate::draft13::message::MessageType::from_id(id).map(|t| t.name()),
110 ("draft14", Draft14) => crate::draft14::message::MessageType::from_id(id).map(|t| t.name()),
111 ("draft15", Draft15) => crate::draft15::message::MessageType::from_id(id).map(|t| t.name()),
112 ("draft16", Draft16) => crate::draft16::message::MessageType::from_id(id).map(|t| t.name()),
113 ("draft17", Draft17) => crate::draft17::message::MessageType::from_id(id).map(|t| t.name()),
114 ("draft18", Draft18) => crate::draft18::message::MessageType::from_id(id).map(|t| t.name()),
115 ("draft19", Draft19) => crate::draft19::message::MessageType::from_id(id).map(|t| t.name()),
116 ("draft20", Draft20) => crate::draft20::message::MessageType::from_id(id).map(|t| t.name()),
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 /// The ids that moved, stated as the sequence they moved through. The
125 /// corpus sweep in `tests/message_type_names.rs` is what checks every id on
126 /// every draft; this is the handful that a draft-blind table gets wrong,
127 /// written out so the reason this function takes a draft is visible in the
128 /// crate itself.
129 ///
130 /// Gated on the three drafts it names. [`message_type_name`] answers `None`
131 /// for a draft no feature flag compiled in, which is the right answer and
132 /// not one this test can tell from a wrong table — so under a feature set
133 /// missing any of the three it would fail for a reason that has nothing to
134 /// do with the ids.
135 #[cfg(all(feature = "draft07", feature = "draft14", feature = "draft20"))]
136 #[test]
137 fn reused_ids_answer_per_draft() {
138 let reused: [(u64, [(u8, &str); 3]); 4] = [
139 (0x07, [(7, "announce_ok"), (14, "publish_namespace_ok"), (20, "request_ok")]),
140 (0x08, [(7, "announce_error"), (14, "publish_namespace_error"), (20, "namespace")]),
141 (0x0E, [(7, "track_status"), (14, "track_status_ok"), (20, "namespace_done")]),
142 (0x0B, [(7, "subscribe_done"), (14, "publish_done"), (20, "publish_done")]),
143 ];
144 for (id, expected) in reused {
145 for (draft, name) in expected {
146 assert_eq!(message_type_name(draft, id), Some(name), "draft-{draft} {id:#x}");
147 }
148 }
149 }
150
151 /// `track_status` names opposite sides of the exchange either side of
152 /// draft-13, and the two ids swap under it. What keeps the module's table
153 /// from drifting.
154 #[cfg(all(feature = "draft11", feature = "draft14"))]
155 #[test]
156 fn one_name_reverses_role_at_draft13() {
157 assert_eq!(message_type_name(11, 0x0D), Some("track_status_request"));
158 assert_eq!(message_type_name(11, 0x0E), Some("track_status"));
159
160 assert_eq!(message_type_name(14, 0x0D), Some("track_status"));
161 assert_eq!(message_type_name(14, 0x0E), Some("track_status_ok"));
162 }
163
164 #[test]
165 fn none_outside_the_implemented_drafts() {
166 for draft in [0u8, 6, 21, 255] {
167 assert_eq!(message_type_name(draft, 0x03), None, "draft {draft}");
168 }
169 }
170
171 /// An id no draft in the range assigns. 0x3F is what the corpus's
172 /// `unknown-type.json` vectors put on the wire for exactly this.
173 ///
174 /// Needs no feature gate: a draft that is not compiled in answers `None`
175 /// for every id, which is what this asserts anyway.
176 #[test]
177 fn none_for_an_unassigned_id() {
178 for draft in 7..=20u8 {
179 assert_eq!(message_type_name(draft, 0x3F), None, "draft-{draft}");
180 }
181 }
182}