moqtap_codec/setup_option_names.rs
1//! The name a draft gives a setup parameter.
2//!
3//! The sibling of [`crate::message_names`], and per draft for the same reason:
4//! the codepoints are reused and retired rather than reserved. `0x02` is
5//! `max_subscribe_id` through draft-10, `max_request_id` from draft-11 to
6//! draft-16, and nothing at all from draft-17, which removed it and left the
7//! number unassigned. A table keyed on the number alone cannot say that.
8//!
9//! # Why this takes a whole parameter and not a key
10//!
11//! Drafts 11 through 13 name a setup parameter from its key *and the shape of
12//! its value*: `0x01` carrying bytes is `path`, and `0x01` carrying a varint is
13//! nothing those drafts define. A `fn(u64) -> Option<&str>` would have to
14//! guess, so the whole [`KeyValuePair`] goes in and the draft's own renderer
15//! answers.
16//!
17//! That renderer is the same one [`crate::dispatch::AnyControlMessage::fields`]
18//! uses, which is the property worth having: a parameter this reports as named
19//! is a parameter that draft would have named in a decoded message, because it
20//! is the same code answering.
21//!
22//! # What it is for
23//!
24//! Asking a *second* draft about a parameter that arrived under a first. A
25//! relay that sends a setup parameter the draft it negotiated does not define
26//! is sending something — an extension, or code left over from the draft it
27//! used to speak — and which of those it is depends on whether some other draft
28//! names it. That is an upgrade-hygiene signal: it is a fingerprint and also a
29//! line an operator can act on.
30//!
31//! Receivers must ignore setup parameters they do not recognise, so a leftover
32//! is never a protocol violation. It is still a fact about the sender.
33//!
34//! # The dispatch is shared; the answer is not
35//!
36//! The one-arm-per-draft, catch-all-free `match` this dispatch needs is
37//! `crate::draft_table::by_draft` — private, so not a link — and
38//! [`crate::message_names`] spells the
39//! same one. Only the dispatch is shared: this table takes a whole parameter,
40//! answers a `String` out of the draft's own renderer, and has its own reasons
41//! for both. See `draft_table` for the argument the two share.
42
43use crate::kvp::KeyValuePair;
44
45/// The name draft `draft` gives `param` in a setup message, or `None` if that
46/// draft defines nothing for it.
47///
48/// `draft` is the draft number as the IETF writes it — 7 through 20 — matching
49/// [`crate::version::DraftVersion::number`]. A number outside the range this
50/// crate implements answers `None`, as does a draft whose feature flag is off in
51/// this build.
52///
53/// This is the *setup* parameter namespace, which is not the one non-setup
54/// messages use: `0x02` is `max_request_id` in a draft-14 SERVER_SETUP and
55/// `delivery_timeout` in a draft-14 SUBSCRIBE. Asking this about a parameter
56/// that arrived on a request would name the wrong thing.
57///
58/// ```
59/// use moqtap_codec::kvp::{KeyValuePair, KvpValue};
60/// use moqtap_codec::setup_option_name;
61/// use moqtap_codec::varint::VarInt;
62///
63/// # #[cfg(all(feature = "draft10", feature = "draft14", feature = "draft18"))]
64/// # {
65/// let max = KeyValuePair {
66/// key: VarInt::from_u64(0x02).unwrap(),
67/// value: KvpValue::Varint(VarInt::from_u64(100).unwrap()),
68/// };
69/// // The same codepoint, renamed once and then retired.
70/// assert_eq!(setup_option_name(10, &max).as_deref(), Some("max_subscribe_id"));
71/// assert_eq!(setup_option_name(14, &max).as_deref(), Some("max_request_id"));
72/// assert_eq!(setup_option_name(18, &max), None);
73/// # }
74/// ```
75// Both allows are the same fact about the same build, and it is the zero-draft
76// one: `--no-default-features` with no `draftNN` named, which
77// `just test-features` compiles as its own row and `just draft-matrix` compiles
78// twice more. There every arm takes its `#[cfg(not(feature = ...))]` form, so
79// nothing reads `one` and every arm diverges — which makes the whole `match` `!`
80// and the call after it unreachable.
81//
82// An `allow` rather than a `cfg`, and the choice is forced rather than
83// preferred. Spelling the condition would mean writing
84// `any(feature = "draft07", ..., feature = "draft22", feature = "draft23")` — every draft — beside a
85// list of the same drafts, with nothing holding the two level. That is the
86// drift `scripts/check-draft-parity.py` exists to catch and would not catch
87// here: a new draft added to the table and forgotten in the `cfg` compiles
88// clean and silently stops naming anything.
89//
90// Neither allow can hide a defect in a build that has a draft. With one draft
91// enabled that draft's arm is the renderer, so `one` is read and the match
92// returns a value; the allows are inert everywhere except the build that
93// compiled no draft at all, where there is nothing left for them to conceal.
94#[allow(unused_variables)]
95#[allow(unreachable_code)]
96pub fn setup_option_name(draft: u8, param: &KeyValuePair) -> Option<String> {
97 let one = std::slice::from_ref(param);
98 // `return None` rather than `None` as the absent answer: the arms produce a
99 // `FieldValue` to be read by `name_in` below, and "this build has no table
100 // for that draft" is not a rendering of anything. See
101 // `crate::draft_table::by_draft`.
102 let rendered = crate::draft_table::by_draft! { draft, return None,
103 // Drafts 08 through 10 share draft-08's table, which is the same two
104 // parameters draft-07 has minus ROLE.
105 ("draft07", Draft07) => crate::fields::params::kvp_to_json_d07_setup(one),
106 ("draft08", Draft08) => crate::fields::params::kvp_to_json_d08_setup(one),
107 ("draft09", Draft09) => crate::fields::params::kvp_to_json_d08_setup(one),
108 ("draft10", Draft10) => crate::fields::params::kvp_to_json_d08_setup(one),
109 ("draft11", Draft11) => crate::draft11::fields::kvp_to_json_setup(one),
110 ("draft12", Draft12) => crate::draft12::fields::kvp_to_json_setup(one),
111 ("draft13", Draft13) => crate::draft13::fields::kvp_to_json_setup(one),
112 ("draft14", Draft14) => crate::draft14::fields::kvp_to_json_d14_setup(one),
113 ("draft15", Draft15) => crate::draft15::fields::kvp_to_json_d15_setup(one),
114 ("draft16", Draft16) => crate::draft16::fields::kvp_to_json_d16_setup(one),
115 // Draft-17 renamed the block to Setup Options and unified the message;
116 // the renderer's name follows the draft's word for it.
117 ("draft17", Draft17) => crate::draft17::fields::options_to_json(one),
118 ("draft18", Draft18) => crate::draft18::fields::options_to_json(one),
119 ("draft19", Draft19) => crate::draft19::fields::options_to_json(one),
120 ("draft20", Draft20) => crate::draft20::fields::options_to_json(one),
121 ("draft21", Draft21) => crate::draft21::fields::options_to_json(one),
122 ("draft22", Draft22) => crate::draft22::fields::options_to_json(one),
123 ("draft23", Draft23) => crate::draft23::fields::options_to_json(one),
124 };
125 name_in(&rendered)
126}
127
128/// The `name` of the single entry a one-parameter render produced.
129///
130/// `kvp_entries` writes `name` only for a parameter the draft defines, which is
131/// the whole question — so an entry without one is the `None` this returns.
132fn name_in(rendered: &crate::fields::FieldValue) -> Option<String> {
133 use crate::fields::FieldValue;
134 let FieldValue::Array(entries) = rendered else {
135 return None;
136 };
137 let FieldValue::Map(entry) = entries.first()? else {
138 return None;
139 };
140 match entry.get("name") {
141 Some(FieldValue::Text(name)) => Some(name.clone()),
142 _ => None,
143 }
144}
145
146#[cfg(test)]
147/// Every positive claim here is gated on the draft that makes it.
148///
149/// [`setup_option_name`] answers `None` for a draft this build did not compile,
150/// by design and stated in its own rustdoc — so an ungated
151/// `assert_eq!(setup_option_name(11, ..), Some("path"))` is not a claim about
152/// the naming table at all under `--no-default-features --features draft07`. It
153/// is a claim that draft-11 was compiled, failing in a build that never
154/// promised to have it. `just test-features` runs this suite once per draft
155/// alone, plus `draft07,draft22` and `draft13,draft14`, so every run that does
156/// not enable draft-11 meets exactly that.
157///
158/// The gates are per **draft asserted**, not per test, wherever one test spans
159/// several: `a_retired_codepoint_is_named_by_the_drafts_that_had_it_and_no_others`
160/// is a statement about four drafts and keeps whichever of them this build has,
161/// rather than being switched off whole because one is missing.
162///
163/// The two negative claims — a codepoint nobody assigned, and a draft number
164/// outside the implemented range — are left ungated because they are true under every feature
165/// set and no build can make them false. They are *vacuous* for a draft that
166/// was left out, since it answers `None` for every parameter; that is a
167/// weakening the all-drafts `cargo test --workspace` run in `just test` covers,
168/// and it is the reason those two are not the whole of this module's coverage.
169mod tests {
170 use super::*;
171 use crate::kvp::KvpValue;
172 use crate::varint::VarInt;
173
174 fn varint(key: u64, value: u64) -> KeyValuePair {
175 KeyValuePair {
176 key: VarInt::from_u64(key).unwrap(),
177 value: KvpValue::Varint(VarInt::from_u64(value).unwrap()),
178 }
179 }
180
181 fn bytes(key: u64, value: &[u8]) -> KeyValuePair {
182 KeyValuePair { key: VarInt::from_u64(key).unwrap(), value: KvpValue::Bytes(value.to_vec()) }
183 }
184
185 /// The case this exists for: a codepoint one draft removed.
186 ///
187 /// moxygen sends `0x02` on draft-18, which draft-17 deleted. Reading it as
188 /// unknown is right about draft-18 and says nothing useful; asking the
189 /// earlier drafts is what turns it into "MAX_REQUEST_ID, left behind".
190 ///
191 /// Three positive claims, one per draft, each kept only in a build that has
192 /// that draft — see the note on this module. The retirement loop stays
193 /// ungated: `None` is the right answer for 17 through 20 whether they were
194 /// compiled or not, so no feature set can make it wrong, and the build that
195 /// makes it *mean* something is any one with a draft in that range.
196 #[test]
197 fn a_retired_codepoint_is_named_by_the_drafts_that_had_it_and_no_others() {
198 let max = varint(0x02, 100);
199 #[cfg(feature = "draft10")]
200 assert_eq!(setup_option_name(10, &max).as_deref(), Some("max_subscribe_id"));
201 #[cfg(feature = "draft11")]
202 assert_eq!(setup_option_name(11, &max).as_deref(), Some("max_request_id"));
203 #[cfg(feature = "draft16")]
204 assert_eq!(setup_option_name(16, &max).as_deref(), Some("max_request_id"));
205 for draft in
206 crate::version::DraftVersion::ALL.map(|d| d.number()).into_iter().filter(|n| *n >= 17)
207 {
208 assert_eq!(setup_option_name(draft, &max), None, "draft-{draft} still names 0x02");
209 }
210 }
211
212 /// A codepoint no draft in the range has ever assigned.
213 #[test]
214 fn an_unassigned_codepoint_is_named_by_nobody() {
215 let odd = bytes(0x21, b"\x01\xff");
216 for draft in crate::version::DraftVersion::ALL.map(|d| d.number()) {
217 assert_eq!(setup_option_name(draft, &odd), None, "draft-{draft} names 0x21");
218 }
219 }
220
221 /// Drafts 11-13 name from the value's shape as well as the key, which is
222 /// why this takes a parameter rather than a number.
223 ///
224 /// Wholly a draft-11 claim — both halves, including the negative one: a
225 /// build without draft-11 answers `None` to the varint case for the reason
226 /// this test is *not* about, so keeping that half alone would read as
227 /// evidence for a rule the build cannot state. Gated whole rather than
228 /// per-assertion, which is what tells the two apart.
229 #[cfg(feature = "draft11")]
230 #[test]
231 fn a_name_can_depend_on_the_shape_of_the_value() {
232 assert_eq!(setup_option_name(11, &bytes(0x01, b"/moq")).as_deref(), Some("path"));
233 assert_eq!(
234 setup_option_name(11, &varint(0x01, 4)),
235 None,
236 "draft-11 defines PATH as bytes and nothing as a varint"
237 );
238 }
239
240 /// The setup namespace, and not the one requests use. `0x02` is
241 /// `max_request_id` in a draft-14 setup and `delivery_timeout` in a
242 /// draft-14 SUBSCRIBE, and answering the second here would be wrong in a
243 /// way nothing downstream could catch.
244 ///
245 /// One draft's claim, so one draft's gate. The two namespaces it separates
246 /// are both draft-14's, and a build without draft-14 has neither of them to
247 /// confuse.
248 #[cfg(feature = "draft14")]
249 #[test]
250 fn the_answer_is_the_setup_namespaces_and_not_the_message_ones() {
251 assert_eq!(setup_option_name(14, &varint(0x02, 100)).as_deref(), Some("max_request_id"));
252 }
253
254 /// A draft number outside the range answers `None` rather than panicking.
255 #[test]
256 fn a_draft_this_crate_does_not_implement_names_nothing() {
257 let max = varint(0x02, 100);
258 assert_eq!(setup_option_name(6, &max), None);
259 let beyond = crate::version::DraftVersion::newest().number() + 1;
260 assert_eq!(setup_option_name(beyond, &max), None);
261 }
262}