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 fourteen-armed, 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 = "draft20")` — all fourteen — beside a
85// list of the same fourteen, 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 fifteenth 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 };
122 name_in(&rendered)
123}
124
125/// The `name` of the single entry a one-parameter render produced.
126///
127/// `kvp_entries` writes `name` only for a parameter the draft defines, which is
128/// the whole question — so an entry without one is the `None` this returns.
129fn name_in(rendered: &crate::fields::FieldValue) -> Option<String> {
130 use crate::fields::FieldValue;
131 let FieldValue::Array(entries) = rendered else {
132 return None;
133 };
134 let FieldValue::Map(entry) = entries.first()? else {
135 return None;
136 };
137 match entry.get("name") {
138 Some(FieldValue::Text(name)) => Some(name.clone()),
139 _ => None,
140 }
141}
142
143#[cfg(test)]
144/// Every positive claim here is gated on the draft that makes it.
145///
146/// [`setup_option_name`] answers `None` for a draft this build did not compile,
147/// by design and stated in its own rustdoc — so an ungated
148/// `assert_eq!(setup_option_name(11, ..), Some("path"))` is not a claim about
149/// the naming table at all under `--no-default-features --features draft07`. It
150/// is a claim that draft-11 was compiled, failing in a build that never
151/// promised to have it. `just test-features` runs this suite fourteen times,
152/// once per draft alone, plus `draft07,draft20` and `draft13,draft14`, so
153/// thirteen of those runs meet exactly that.
154///
155/// The gates are per **draft asserted**, not per test, wherever one test spans
156/// several: `a_retired_codepoint_is_named_by_the_drafts_that_had_it_and_no_others`
157/// is a statement about four drafts and keeps whichever of them this build has,
158/// rather than being switched off whole because one is missing.
159///
160/// The two negative claims — a codepoint nobody assigned, and a draft number
161/// outside 7..=20 — are left ungated because they are true under every feature
162/// set and no build can make them false. They are *vacuous* for a draft that
163/// was left out, since it answers `None` for every parameter; that is a
164/// weakening the all-drafts `cargo test --workspace` run in `just test` covers,
165/// and it is the reason those two are not the whole of this module's coverage.
166mod tests {
167 use super::*;
168 use crate::kvp::KvpValue;
169 use crate::varint::VarInt;
170
171 fn varint(key: u64, value: u64) -> KeyValuePair {
172 KeyValuePair {
173 key: VarInt::from_u64(key).unwrap(),
174 value: KvpValue::Varint(VarInt::from_u64(value).unwrap()),
175 }
176 }
177
178 fn bytes(key: u64, value: &[u8]) -> KeyValuePair {
179 KeyValuePair { key: VarInt::from_u64(key).unwrap(), value: KvpValue::Bytes(value.to_vec()) }
180 }
181
182 /// The case this exists for: a codepoint one draft removed.
183 ///
184 /// moxygen sends `0x02` on draft-18, which draft-17 deleted. Reading it as
185 /// unknown is right about draft-18 and says nothing useful; asking the
186 /// earlier drafts is what turns it into "MAX_REQUEST_ID, left behind".
187 ///
188 /// Three positive claims, one per draft, each kept only in a build that has
189 /// that draft — see the note on this module. The retirement loop stays
190 /// ungated: `None` is the right answer for 17 through 20 whether they were
191 /// compiled or not, so no feature set can make it wrong, and the build that
192 /// makes it *mean* something is any one with a draft in that range.
193 #[test]
194 fn a_retired_codepoint_is_named_by_the_drafts_that_had_it_and_no_others() {
195 let max = varint(0x02, 100);
196 #[cfg(feature = "draft10")]
197 assert_eq!(setup_option_name(10, &max).as_deref(), Some("max_subscribe_id"));
198 #[cfg(feature = "draft11")]
199 assert_eq!(setup_option_name(11, &max).as_deref(), Some("max_request_id"));
200 #[cfg(feature = "draft16")]
201 assert_eq!(setup_option_name(16, &max).as_deref(), Some("max_request_id"));
202 for draft in 17..=20 {
203 assert_eq!(setup_option_name(draft, &max), None, "draft-{draft} still names 0x02");
204 }
205 }
206
207 /// A codepoint no draft in the range has ever assigned.
208 #[test]
209 fn an_unassigned_codepoint_is_named_by_nobody() {
210 let odd = bytes(0x21, b"\x01\xff");
211 for draft in 7..=20 {
212 assert_eq!(setup_option_name(draft, &odd), None, "draft-{draft} names 0x21");
213 }
214 }
215
216 /// Drafts 11-13 name from the value's shape as well as the key, which is
217 /// why this takes a parameter rather than a number.
218 ///
219 /// Wholly a draft-11 claim — both halves, including the negative one: a
220 /// build without draft-11 answers `None` to the varint case for the reason
221 /// this test is *not* about, so keeping that half alone would read as
222 /// evidence for a rule the build cannot state. Gated whole rather than
223 /// per-assertion, which is what tells the two apart.
224 #[cfg(feature = "draft11")]
225 #[test]
226 fn a_name_can_depend_on_the_shape_of_the_value() {
227 assert_eq!(setup_option_name(11, &bytes(0x01, b"/moq")).as_deref(), Some("path"));
228 assert_eq!(
229 setup_option_name(11, &varint(0x01, 4)),
230 None,
231 "draft-11 defines PATH as bytes and nothing as a varint"
232 );
233 }
234
235 /// The setup namespace, and not the one requests use. `0x02` is
236 /// `max_request_id` in a draft-14 setup and `delivery_timeout` in a
237 /// draft-14 SUBSCRIBE, and answering the second here would be wrong in a
238 /// way nothing downstream could catch.
239 ///
240 /// One draft's claim, so one draft's gate. The two namespaces it separates
241 /// are both draft-14's, and a build without draft-14 has neither of them to
242 /// confuse.
243 #[cfg(feature = "draft14")]
244 #[test]
245 fn the_answer_is_the_setup_namespaces_and_not_the_message_ones() {
246 assert_eq!(setup_option_name(14, &varint(0x02, 100)).as_deref(), Some("max_request_id"));
247 }
248
249 /// A draft number outside the range answers `None` rather than panicking.
250 #[test]
251 fn a_draft_this_crate_does_not_implement_names_nothing() {
252 let max = varint(0x02, 100);
253 assert_eq!(setup_option_name(6, &max), None);
254 assert_eq!(setup_option_name(21, &max), None);
255 }
256}