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 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}