Skip to main content

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}