Skip to main content

moqtap_codec/draft20/
fields.rs

1use crate::draft20::message::ControlMessage;
2use crate::fields::{FieldMap as Map, FieldValue as Value};
3use crate::kvp::{KeyValuePair, KvpValue};
4use crate::range_filter::RangeFilter;
5use crate::types::*;
6use crate::varint::{Moqt18 as Wire, VarInt};
7
8fn vi(v: u64) -> Value {
9    Value::Uint(v)
10}
11
12fn ns_to_json(ns: &TrackNamespace) -> Value {
13    Value::Array(
14        ns.0.iter().map(|e| Value::Text(String::from_utf8_lossy(e).into_owned())).collect(),
15    )
16}
17
18// Draft-20 known parameter types and their encodings. Two rows are new:
19// 0x23 FILL_PARAMETERS and 0x35 INCLUDE_PROPERTIES. Nothing was removed or
20// renumbered.
21fn d20_param_name(key: u64) -> Option<&'static str> {
22    match key {
23        0x02 => Some("object_delivery_timeout"),
24        0x03 => Some("authorization_token"),
25        0x04 => Some("rendezvous_timeout"),
26        0x06 => Some("subgroup_delivery_timeout"),
27        0x08 => Some("expires"),
28        0x09 => Some("largest_object"),
29        0x0A => Some("fill_timeout"),
30        0x10 => Some("forward"),
31        0x20 => Some("subscriber_priority"),
32        0x21 => Some("location_filter"),
33        0x22 => Some("group_order"),
34        0x23 => Some("fill_parameters"),
35        0x25 => Some("subgroup_filter"),
36        0x26 => Some("objectid_filter"),
37        0x27 => Some("priority_filter"),
38        0x28 => Some("object_property_filter"),
39        0x29 => Some("track_property_filter"),
40        0x32 => Some("new_group_request"),
41        0x34 => Some("track_namespace_prefix"),
42        0x35 => Some("include_properties"),
43        _ => None,
44    }
45}
46
47// Draft-20 setup option names — unchanged from draft-19.
48fn d20_option_name(key: u64) -> Option<&'static str> {
49    match key {
50        0x01 => Some("path"),
51        0x03 => Some("authorization_token"),
52        0x04 => Some("max_auth_token_cache_size"),
53        0x05 => Some("authority"),
54        0x06 => Some("max_filter_ranges"),
55        0x07 => Some("moqt_implementation"),
56        0x08 => Some("max_request_updates"),
57        _ => None,
58    }
59}
60
61/// Render a draft-20 Range Filter parameter value: SetID (u8), an optional
62/// Property Type (varint, for the Object/Track Property filters), then a
63/// sequence of delta-encoded inclusive Start/End range pairs. A zero-length
64/// value denotes filter removal (only meaningful in REQUEST_UPDATE).
65///
66/// Unchanged from draft-19: draft-20 replaced the one-line shorthand of its
67/// Section 5.1.3 with proper figures in Section 5.1.4 and changed no codepoint,
68/// field order or delta arithmetic.
69///
70/// # The parse belongs to [`crate::range_filter`], not here
71///
72/// Draft-20's parameter table gives 0x25-0x29 `LengthPrefixed`, which stores
73/// the value verbatim, and `check_location_filters` covers 0x21 and 0x23 and
74/// nothing else — so the bytes arriving here are whatever a peer sent, one
75/// level down inside `FILL_PARAMETERS` as well as at the top. A parse written
76/// out again here read its varints with `unwrap` and resolved its two delta
77/// baselines with `+`, on a value where `Buf::has_remaining` promises one more
78/// byte and a MoQT varint may need nine. The bare `+` was the worse half: in
79/// release it wrapped rather than panicking, and a filter recorded as
80/// `start = u64::MAX, end = 0` is a wrong answer that looks like data.
81///
82/// [`RangeFilter::decode_moqt`] is the same read done once, with `checked_add`
83/// on both baselines and a `Malformed` for every field the value ends before.
84/// Calling it makes the checked parse the only parse.
85///
86/// # What a value it cannot read renders as
87///
88/// The raw bytes. [`message_fields`] answers for a message that has *already*
89/// decoded, so refusing is not available to it: the frame is valid and one
90/// parameter's value is not. Rendering what arrived is the answer
91/// `fields::params` gives in the same situation, and the one this file's own
92/// `auth_token_to_json_d20` and `decode_track_namespace_prefix` already give.
93///
94/// # A filter that parses but breaks a content rule still renders
95///
96/// [`RangeFilter`] enforces two rules beyond the value's shape — a Publisher
97/// Priority range above 255, and a property filter over an odd Property Type.
98/// Draft-20 answers both with REQUEST_ERROR rather than a session close, so
99/// both arrive here as bytes a peer really sent.
100///
101/// Those two are exactly the filters whose fields a reader most needs to see,
102/// so this renders them and names the rule broken under `violates`, rather
103/// than refusing and hiding the offending value inside a hex dump. That is why
104/// it decodes with [`RangeFilter::decode_moqt_structure`] and asks
105/// [`RangeFilter::check_its_own_types`] separately: a decoder must refuse these
106/// values, a renderer must describe them, and the split is by what the caller
107/// does with the answer rather than by how much checking it wants.
108fn decode_range_filter(bytes: &[u8], parameter_type: u64) -> Value {
109    let mut o = Map::new();
110    if bytes.is_empty() {
111        o.insert("removed".into(), Value::Bool(true));
112        return Value::Map(o);
113    }
114    let Ok(filter) = RangeFilter::decode_moqt_structure::<Wire>(parameter_type, bytes) else {
115        return Value::Bytes(bytes.to_vec());
116    };
117    if let Err(broken) = filter.check_its_own_types() {
118        o.insert("violates".into(), Value::Text(broken.to_string()));
119    }
120    o.insert("set_id".into(), vi(filter.set_id as u64));
121    if let Some(property_type) = filter.property_type {
122        o.insert("property_type".into(), vi(property_type));
123    }
124    let ranges = filter
125        .ranges
126        .iter()
127        .map(|range| {
128            let mut r = Map::new();
129            r.insert("start".into(), vi(range.start));
130            if let Some(end) = range.end {
131                r.insert("end".into(), vi(end));
132            }
133            Value::Map(r)
134        })
135        .collect();
136    o.insert("ranges".into(), Value::Array(ranges));
137    Value::Map(o)
138}
139
140/// Decode a draft-20 LOCATION_FILTER value.
141///
142/// The Filter Type enum is gone — draft-19's `0x1` Next Group Start, `0x2`
143/// Largest Object, `0x3` AbsoluteStart and `0x4` AbsoluteRange — and the shape
144/// comes from how many `vi64` fields the value holds instead (Section 5.1.2).
145/// The names are positional: one field is `StartGroup`, two adds
146/// `StartObject`, three adds `EndGroupDelta`, four adds `EndObject`, and a
147/// zero-length value is the filter's removal.
148///
149/// The fields are decoded through the codec rather than restated here, so this
150/// reports what the codec read.
151fn decode_location_filter(bytes: &[u8]) -> Value {
152    const NAMES: [&str; 4] = ["start_group", "start_object", "end_group_delta", "end_object"];
153    let fields = crate::draft20::message::decode_location_filter(bytes)
154        .expect("the decoder accepted this parameter, so its value parses");
155    let mut obj = Map::new();
156    if fields.is_empty() {
157        obj.insert("removed".into(), Value::Bool(true));
158        return Value::Map(obj);
159    }
160    for (name, value) in NAMES.iter().zip(fields) {
161        obj.insert((*name).to_string(), vi(value));
162    }
163    Value::Map(obj)
164}
165
166/// Decode a draft-20 FILL_PARAMETERS value: a nested parameter block in its own
167/// scope, reported the same way an outer block is.
168///
169/// The nesting is what makes this recursive with [`params_to_json`] — a fill's
170/// LOCATION_FILTER is rendered exactly as the subscription's is — and it
171/// bottoms out because Table 6 does not list FILL_PARAMETERS inside itself.
172fn decode_fill_parameters(bytes: &[u8]) -> Value {
173    let nested = crate::draft20::message::decode_fill_parameters(bytes)
174        .expect("the decoder accepted this parameter, so its value parses");
175    params_to_json(&nested)
176}
177
178fn auth_token_to_json_d20(bytes: &[u8]) -> Value {
179    let mut buf = bytes;
180    let alias_type = match VarInt::decode_moqt::<Wire>(&mut buf) {
181        Ok(v) => v,
182        Err(_) => return Value::Bytes(bytes.to_vec()),
183    };
184    let at = alias_type.into_inner();
185    let mut o = Map::new();
186    o.insert("alias_type".into(), vi(at));
187    match at {
188        0 | 2 => {
189            if let Ok(ta) = VarInt::decode_moqt::<Wire>(&mut buf) {
190                o.insert("token_alias".into(), vi(ta.into_inner()));
191            }
192        }
193        1 => {
194            if let Ok(ta) = VarInt::decode_moqt::<Wire>(&mut buf) {
195                o.insert("token_alias".into(), vi(ta.into_inner()));
196            }
197            if let Ok(tt) = VarInt::decode_moqt::<Wire>(&mut buf) {
198                o.insert("token_type".into(), vi(tt.into_inner()));
199            }
200            // Draft-18 onwards: token_value runs to end of bytes (no inner
201            // length).
202            o.insert("token_value".into(), Value::Bytes(buf.to_vec()));
203        }
204        _ => {
205            if let Ok(tt) = VarInt::decode_moqt::<Wire>(&mut buf) {
206                o.insert("token_type".into(), vi(tt.into_inner()));
207            }
208            o.insert("token_value".into(), Value::Bytes(buf.to_vec()));
209        }
210    }
211    Value::Map(o)
212}
213
214fn decode_largest_object(bytes: &[u8]) -> Value {
215    let mut buf = bytes;
216    let group = VarInt::decode_moqt::<Wire>(&mut buf).unwrap().into_inner();
217    let object = VarInt::decode_moqt::<Wire>(&mut buf).unwrap().into_inner();
218    let mut obj = Map::new();
219    obj.insert("group".into(), vi(group));
220    obj.insert("object".into(), vi(object));
221    Value::Map(obj)
222}
223
224fn decode_track_namespace_prefix(bytes: &[u8]) -> Value {
225    let mut buf = bytes;
226    match TrackNamespace::decode_allow_empty_moqt::<Wire>(&mut buf) {
227        Ok(ns) => ns_to_json(&ns),
228        Err(_) => Value::Bytes(bytes.to_vec()),
229    }
230}
231
232fn params_to_json(params: &[KeyValuePair]) -> Value {
233    crate::fields::kvp_entries(params, |key, value| {
234        let Some(name) = d20_param_name(key) else {
235            return (None, None);
236        };
237        let rendered = match (value, key) {
238            (KvpValue::Bytes(b), 0x21) => decode_location_filter(b),
239            (KvpValue::Bytes(b), 0x23) => decode_fill_parameters(b),
240            // One arm for all five: which of them carries a Property Type
241            // is a property of the type, and `range_filter` is the one
242            // place that decides it. Two arms passing a bool were two
243            // chances to answer it differently.
244            (KvpValue::Bytes(b), 0x25..=0x29) => decode_range_filter(b, key),
245            (KvpValue::Bytes(b), 0x09) => decode_largest_object(b),
246            (KvpValue::Bytes(b), 0x34) => decode_track_namespace_prefix(b),
247            (KvpValue::Bytes(b), _) if name == "authorization_token" => auth_token_to_json_d20(b),
248            (KvpValue::Varint(v), _) => vi(v.into_inner()),
249            (KvpValue::Bytes(b), _) => Value::Text(String::from_utf8_lossy(b).into_owned()),
250        };
251        (Some(name), Some(rendered))
252    })
253}
254
255fn options_to_json(options: &[KeyValuePair]) -> Value {
256    crate::fields::kvp_entries(options, |key, value| {
257        let Some(name) = d20_option_name(key) else {
258            return (None, None);
259        };
260        let rendered = match value {
261            KvpValue::Varint(v) => vi(v.into_inner()),
262            KvpValue::Bytes(b) if name == "authorization_token" => auth_token_to_json_d20(b),
263            KvpValue::Bytes(b) => Value::Text(String::from_utf8_lossy(b).into_owned()),
264        };
265        (Some(name), Some(rendered))
266    })
267}
268
269fn d20_track_prop_name(key: u64) -> Option<&'static str> {
270    match key {
271        0x02 => Some("object_delivery_timeout"),
272        0x04 => Some("max_cache_duration"),
273        0x06 => Some("subgroup_delivery_timeout"),
274        0x0b => Some("immutable_properties"),
275        0x0e => Some("default_publisher_priority"),
276        0x22 => Some("default_publisher_group_order"),
277        0x30 => Some("dynamic_groups"),
278        _ => None,
279    }
280}
281
282fn track_props_to_json(props: &[KeyValuePair]) -> Value {
283    crate::fields::kvp_entries(props, |key, value| {
284        let name = d20_track_prop_name(key);
285        let rendered = match value {
286            KvpValue::Varint(v) => Some(vi(v.into_inner())),
287            // A property this draft does not name keeps its bytes rather than
288            // a name invented from its type, which is what an entry's absent
289            // `name` already says.
290            KvpValue::Bytes(_) => None,
291        };
292        (name, rendered)
293    })
294}
295
296/// This draft's field names for a decoded control message.
297///
298/// Keys are the names this draft gives its fields, in the order it defines
299/// them. An optional field the message did not carry is absent rather than
300/// zero.
301pub fn message_fields(msg: &ControlMessage) -> Map {
302    let obj = match msg {
303        ControlMessage::Setup(m) => {
304            let mut o = Map::new();
305            o.insert("options".into(), options_to_json(&m.options));
306            o
307        }
308        ControlMessage::GoAway(m) => {
309            let mut o = Map::new();
310            o.insert(
311                "new_session_uri".into(),
312                Value::Text(String::from_utf8_lossy(&m.new_session_uri).into_owned()),
313            );
314            o.insert("timeout".into(), vi(m.timeout.into_inner()));
315            o
316        }
317        ControlMessage::RequestOk(m) => {
318            let mut o = Map::new();
319            o.insert("parameters".into(), params_to_json(&m.parameters));
320            o.insert("track_properties".into(), track_props_to_json(&m.track_properties));
321            o
322        }
323        ControlMessage::RequestError(m) => {
324            let mut o = Map::new();
325            o.insert("error_code".into(), vi(m.error_code.into_inner()));
326            o.insert("retry_interval".into(), vi(m.retry_interval.into_inner()));
327            o.insert(
328                "reason_phrase".into(),
329                Value::Text(String::from_utf8_lossy(&m.reason_phrase).into_owned()),
330            );
331            if let Some(r) = &m.redirect {
332                let mut r_obj = Map::new();
333                r_obj.insert(
334                    "connect_uri".into(),
335                    Value::Text(String::from_utf8_lossy(&r.connect_uri).into_owned()),
336                );
337                r_obj.insert("track_namespace".into(), ns_to_json(&r.track_namespace));
338                r_obj.insert(
339                    "track_name".into(),
340                    Value::Text(String::from_utf8_lossy(&r.track_name).into_owned()),
341                );
342                o.insert("redirect".into(), Value::Map(r_obj));
343            }
344            o
345        }
346        ControlMessage::Subscribe(m) => {
347            let mut o = Map::new();
348            o.insert("request_id".into(), vi(m.request_id.into_inner()));
349            o.insert("track_namespace".into(), ns_to_json(&m.track_namespace));
350            o.insert(
351                "track_name".into(),
352                Value::Text(String::from_utf8_lossy(&m.track_name).into_owned()),
353            );
354            o.insert("parameters".into(), params_to_json(&m.parameters));
355            o
356        }
357        ControlMessage::SubscribeOk(m) => {
358            let mut o = Map::new();
359            o.insert("track_alias".into(), vi(m.track_alias.into_inner()));
360            o.insert("parameters".into(), params_to_json(&m.parameters));
361            o.insert("track_properties".into(), track_props_to_json(&m.track_properties));
362            o
363        }
364        ControlMessage::RequestUpdate(m) => {
365            let mut o = Map::new();
366            o.insert("request_id".into(), vi(m.request_id.into_inner()));
367            o.insert("parameters".into(), params_to_json(&m.parameters));
368            o
369        }
370        ControlMessage::Publish(m) => {
371            let mut o = Map::new();
372            o.insert("request_id".into(), vi(m.request_id.into_inner()));
373            o.insert("track_namespace".into(), ns_to_json(&m.track_namespace));
374            o.insert(
375                "track_name".into(),
376                Value::Text(String::from_utf8_lossy(&m.track_name).into_owned()),
377            );
378            o.insert("track_alias".into(), vi(m.track_alias.into_inner()));
379            o.insert("parameters".into(), params_to_json(&m.parameters));
380            o.insert("track_properties".into(), track_props_to_json(&m.track_properties));
381            o
382        }
383        // New in draft-20, and the only control message with a Parameters field
384        // and nothing else: no Request ID, no Track Properties.
385        ControlMessage::PublishStateNotify(m) => {
386            let mut o = Map::new();
387            o.insert("parameters".into(), params_to_json(&m.parameters));
388            o
389        }
390        ControlMessage::PublishDone(m) => {
391            let mut o = Map::new();
392            o.insert("status_code".into(), vi(m.status_code.into_inner()));
393            o.insert("stream_count".into(), vi(m.stream_count.into_inner()));
394            o.insert(
395                "reason_phrase".into(),
396                Value::Text(String::from_utf8_lossy(&m.reason_phrase).into_owned()),
397            );
398            o
399        }
400        ControlMessage::PublishNamespace(m) => {
401            let mut o = Map::new();
402            o.insert("request_id".into(), vi(m.request_id.into_inner()));
403            o.insert("track_namespace".into(), ns_to_json(&m.track_namespace));
404            o.insert("parameters".into(), params_to_json(&m.parameters));
405            o
406        }
407        ControlMessage::Namespace(m) => {
408            let mut o = Map::new();
409            o.insert("namespace_suffix".into(), ns_to_json(&m.namespace_suffix));
410            o
411        }
412        ControlMessage::NamespaceDone(m) => {
413            let mut o = Map::new();
414            o.insert("namespace_suffix".into(), ns_to_json(&m.namespace_suffix));
415            o
416        }
417        ControlMessage::SubscribeNamespace(m) => {
418            let mut o = Map::new();
419            o.insert("request_id".into(), vi(m.request_id.into_inner()));
420            o.insert("namespace_prefix".into(), ns_to_json(&m.namespace_prefix));
421            o.insert("parameters".into(), params_to_json(&m.parameters));
422            o
423        }
424        ControlMessage::SubscribeTracks(m) => {
425            let mut o = Map::new();
426            o.insert("request_id".into(), vi(m.request_id.into_inner()));
427            o.insert("namespace_prefix".into(), ns_to_json(&m.namespace_prefix));
428            o.insert("parameters".into(), params_to_json(&m.parameters));
429            o
430        }
431        ControlMessage::TrackStatus(m) => {
432            let mut o = Map::new();
433            o.insert("request_id".into(), vi(m.request_id.into_inner()));
434            o.insert("track_namespace".into(), ns_to_json(&m.track_namespace));
435            o.insert(
436                "track_name".into(),
437                Value::Text(String::from_utf8_lossy(&m.track_name).into_owned()),
438            );
439            o.insert("parameters".into(), params_to_json(&m.parameters));
440            o
441        }
442        // Draft-20's FETCH is byte-identical to SUBSCRIBE apart from its type
443        // code: no Fetch Type, no joining pair, no inline range.
444        ControlMessage::Fetch(m) => {
445            let mut o = Map::new();
446            o.insert("request_id".into(), vi(m.request_id.into_inner()));
447            o.insert("track_namespace".into(), ns_to_json(&m.track_namespace));
448            o.insert(
449                "track_name".into(),
450                Value::Text(String::from_utf8_lossy(&m.track_name).into_owned()),
451            );
452            o.insert("parameters".into(), params_to_json(&m.parameters));
453            o
454        }
455        ControlMessage::FetchOk(m) => {
456            let mut o = Map::new();
457            o.insert("end_of_track".into(), vi(m.end_of_track as u64));
458            o.insert("end_group".into(), vi(m.end_group.into_inner()));
459            o.insert("end_object".into(), vi(m.end_object.into_inner()));
460            o.insert("parameters".into(), params_to_json(&m.parameters));
461            o.insert("track_properties".into(), track_props_to_json(&m.track_properties));
462            o
463        }
464        ControlMessage::PublishSkipped(m) => {
465            let mut o = Map::new();
466            o.insert("namespace_suffix".into(), ns_to_json(&m.namespace_suffix));
467            o.insert(
468                "track_name".into(),
469                Value::Text(String::from_utf8_lossy(&m.track_name).into_owned()),
470            );
471            o
472        }
473    };
474    obj
475}