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    let mut obj = Map::new();
234    let mut unknown = Vec::new();
235
236    for p in params {
237        let key = p.key.into_inner();
238        if let Some(name) = d20_param_name(key) {
239            match (&p.value, key) {
240                (KvpValue::Bytes(b), 0x21) => {
241                    obj.insert(name.to_string(), decode_location_filter(b));
242                }
243                (KvpValue::Bytes(b), 0x23) => {
244                    obj.insert(name.to_string(), decode_fill_parameters(b));
245                }
246                // One arm for all five: which of them carries a Property Type
247                // is a property of the type, and `range_filter` is the one
248                // place that decides it. Two arms passing a bool were two
249                // chances to answer it differently.
250                (KvpValue::Bytes(b), 0x25..=0x29) => {
251                    obj.insert(name.to_string(), decode_range_filter(b, key));
252                }
253                (KvpValue::Bytes(b), 0x09) => {
254                    obj.insert(name.to_string(), decode_largest_object(b));
255                }
256                (KvpValue::Bytes(b), 0x34) => {
257                    obj.insert(name.to_string(), decode_track_namespace_prefix(b));
258                }
259                (KvpValue::Bytes(b), _) if name == "authorization_token" => {
260                    obj.insert(name.to_string(), auth_token_to_json_d20(b));
261                }
262                (KvpValue::Varint(v), _) => {
263                    obj.insert(name.to_string(), vi(v.into_inner()));
264                }
265                (KvpValue::Bytes(b), _) => {
266                    obj.insert(
267                        name.to_string(),
268                        Value::Text(String::from_utf8_lossy(b).into_owned()),
269                    );
270                }
271            }
272        } else {
273            let mut entry = Map::new();
274            entry.insert("id".to_string(), Value::Text(format!("0x{:x}", key)));
275            match &p.value {
276                KvpValue::Varint(v) => {
277                    entry.insert("length".to_string(), vi(v.into_inner()));
278                }
279                KvpValue::Bytes(b) => {
280                    entry.insert("length".to_string(), vi(b.len() as u64));
281                    entry.insert("raw_hex".to_string(), Value::Bytes(b.to_vec()));
282                }
283            }
284            unknown.push(Value::Map(entry));
285        }
286    }
287
288    if !unknown.is_empty() {
289        obj.insert("unknown".to_string(), Value::Array(unknown));
290    }
291
292    Value::Map(obj)
293}
294
295fn options_to_json(options: &[KeyValuePair]) -> Value {
296    let mut obj = Map::new();
297    for p in options {
298        let key = p.key.into_inner();
299        if let Some(name) = d20_option_name(key) {
300            match &p.value {
301                KvpValue::Varint(v) => {
302                    obj.insert(name.to_string(), vi(v.into_inner()));
303                }
304                KvpValue::Bytes(b) if name == "authorization_token" => {
305                    obj.insert(name.to_string(), auth_token_to_json_d20(b));
306                }
307                KvpValue::Bytes(b) => {
308                    obj.insert(
309                        name.to_string(),
310                        Value::Text(String::from_utf8_lossy(b).into_owned()),
311                    );
312                }
313            }
314        }
315    }
316    Value::Map(obj)
317}
318
319fn d20_track_prop_name(key: u64) -> Option<&'static str> {
320    match key {
321        0x02 => Some("object_delivery_timeout"),
322        0x04 => Some("max_cache_duration"),
323        0x06 => Some("subgroup_delivery_timeout"),
324        0x0b => Some("immutable_properties"),
325        0x0e => Some("default_publisher_priority"),
326        0x22 => Some("default_publisher_group_order"),
327        0x30 => Some("dynamic_groups"),
328        _ => None,
329    }
330}
331
332fn track_props_to_json(props: &[KeyValuePair]) -> Value {
333    let mut obj = Map::new();
334    for p in props {
335        let key = p.key.into_inner();
336        let name = d20_track_prop_name(key)
337            .map(|s| s.to_string())
338            .unwrap_or_else(|| format!("0x{:x}", key));
339        match &p.value {
340            KvpValue::Varint(v) => {
341                obj.insert(name, vi(v.into_inner()));
342            }
343            KvpValue::Bytes(b) => {
344                obj.insert(name, Value::Bytes(b.to_vec()));
345            }
346        }
347    }
348    Value::Map(obj)
349}
350
351/// This draft's field names for a decoded control message.
352///
353/// Keys are the names this draft gives its fields, in the order it defines
354/// them. An optional field the message did not carry is absent rather than
355/// zero.
356pub fn message_fields(msg: &ControlMessage) -> Map {
357    let obj = match msg {
358        ControlMessage::Setup(m) => {
359            let mut o = Map::new();
360            o.insert("options".into(), options_to_json(&m.options));
361            o
362        }
363        ControlMessage::GoAway(m) => {
364            let mut o = Map::new();
365            o.insert(
366                "new_session_uri".into(),
367                Value::Text(String::from_utf8_lossy(&m.new_session_uri).into_owned()),
368            );
369            o.insert("timeout".into(), vi(m.timeout.into_inner()));
370            o
371        }
372        ControlMessage::RequestOk(m) => {
373            let mut o = Map::new();
374            o.insert("parameters".into(), params_to_json(&m.parameters));
375            o.insert("track_properties".into(), track_props_to_json(&m.track_properties));
376            o
377        }
378        ControlMessage::RequestError(m) => {
379            let mut o = Map::new();
380            o.insert("error_code".into(), vi(m.error_code.into_inner()));
381            o.insert("retry_interval".into(), vi(m.retry_interval.into_inner()));
382            o.insert(
383                "reason_phrase".into(),
384                Value::Text(String::from_utf8_lossy(&m.reason_phrase).into_owned()),
385            );
386            if let Some(r) = &m.redirect {
387                let mut r_obj = Map::new();
388                r_obj.insert(
389                    "connect_uri".into(),
390                    Value::Text(String::from_utf8_lossy(&r.connect_uri).into_owned()),
391                );
392                r_obj.insert("track_namespace".into(), ns_to_json(&r.track_namespace));
393                r_obj.insert(
394                    "track_name".into(),
395                    Value::Text(String::from_utf8_lossy(&r.track_name).into_owned()),
396                );
397                o.insert("redirect".into(), Value::Map(r_obj));
398            }
399            o
400        }
401        ControlMessage::Subscribe(m) => {
402            let mut o = Map::new();
403            o.insert("request_id".into(), vi(m.request_id.into_inner()));
404            o.insert("track_namespace".into(), ns_to_json(&m.track_namespace));
405            o.insert(
406                "track_name".into(),
407                Value::Text(String::from_utf8_lossy(&m.track_name).into_owned()),
408            );
409            o.insert("parameters".into(), params_to_json(&m.parameters));
410            o
411        }
412        ControlMessage::SubscribeOk(m) => {
413            let mut o = Map::new();
414            o.insert("track_alias".into(), vi(m.track_alias.into_inner()));
415            o.insert("parameters".into(), params_to_json(&m.parameters));
416            o.insert("track_properties".into(), track_props_to_json(&m.track_properties));
417            o
418        }
419        ControlMessage::RequestUpdate(m) => {
420            let mut o = Map::new();
421            o.insert("request_id".into(), vi(m.request_id.into_inner()));
422            o.insert("parameters".into(), params_to_json(&m.parameters));
423            o
424        }
425        ControlMessage::Publish(m) => {
426            let mut o = Map::new();
427            o.insert("request_id".into(), vi(m.request_id.into_inner()));
428            o.insert("track_namespace".into(), ns_to_json(&m.track_namespace));
429            o.insert(
430                "track_name".into(),
431                Value::Text(String::from_utf8_lossy(&m.track_name).into_owned()),
432            );
433            o.insert("track_alias".into(), vi(m.track_alias.into_inner()));
434            o.insert("parameters".into(), params_to_json(&m.parameters));
435            o.insert("track_properties".into(), track_props_to_json(&m.track_properties));
436            o
437        }
438        // New in draft-20, and the only control message with a Parameters field
439        // and nothing else: no Request ID, no Track Properties.
440        ControlMessage::PublishStateNotify(m) => {
441            let mut o = Map::new();
442            o.insert("parameters".into(), params_to_json(&m.parameters));
443            o
444        }
445        ControlMessage::PublishDone(m) => {
446            let mut o = Map::new();
447            o.insert("status_code".into(), vi(m.status_code.into_inner()));
448            o.insert("stream_count".into(), vi(m.stream_count.into_inner()));
449            o.insert(
450                "reason_phrase".into(),
451                Value::Text(String::from_utf8_lossy(&m.reason_phrase).into_owned()),
452            );
453            o
454        }
455        ControlMessage::PublishNamespace(m) => {
456            let mut o = Map::new();
457            o.insert("request_id".into(), vi(m.request_id.into_inner()));
458            o.insert("track_namespace".into(), ns_to_json(&m.track_namespace));
459            o.insert("parameters".into(), params_to_json(&m.parameters));
460            o
461        }
462        ControlMessage::Namespace(m) => {
463            let mut o = Map::new();
464            o.insert("namespace_suffix".into(), ns_to_json(&m.namespace_suffix));
465            o
466        }
467        ControlMessage::NamespaceDone(m) => {
468            let mut o = Map::new();
469            o.insert("namespace_suffix".into(), ns_to_json(&m.namespace_suffix));
470            o
471        }
472        ControlMessage::SubscribeNamespace(m) => {
473            let mut o = Map::new();
474            o.insert("request_id".into(), vi(m.request_id.into_inner()));
475            o.insert("namespace_prefix".into(), ns_to_json(&m.namespace_prefix));
476            o.insert("parameters".into(), params_to_json(&m.parameters));
477            o
478        }
479        ControlMessage::SubscribeTracks(m) => {
480            let mut o = Map::new();
481            o.insert("request_id".into(), vi(m.request_id.into_inner()));
482            o.insert("namespace_prefix".into(), ns_to_json(&m.namespace_prefix));
483            o.insert("parameters".into(), params_to_json(&m.parameters));
484            o
485        }
486        ControlMessage::TrackStatus(m) => {
487            let mut o = Map::new();
488            o.insert("request_id".into(), vi(m.request_id.into_inner()));
489            o.insert("track_namespace".into(), ns_to_json(&m.track_namespace));
490            o.insert(
491                "track_name".into(),
492                Value::Text(String::from_utf8_lossy(&m.track_name).into_owned()),
493            );
494            o.insert("parameters".into(), params_to_json(&m.parameters));
495            o
496        }
497        // Draft-20's FETCH is byte-identical to SUBSCRIBE apart from its type
498        // code: no Fetch Type, no joining pair, no inline range.
499        ControlMessage::Fetch(m) => {
500            let mut o = Map::new();
501            o.insert("request_id".into(), vi(m.request_id.into_inner()));
502            o.insert("track_namespace".into(), ns_to_json(&m.track_namespace));
503            o.insert(
504                "track_name".into(),
505                Value::Text(String::from_utf8_lossy(&m.track_name).into_owned()),
506            );
507            o.insert("parameters".into(), params_to_json(&m.parameters));
508            o
509        }
510        ControlMessage::FetchOk(m) => {
511            let mut o = Map::new();
512            o.insert("end_of_track".into(), vi(m.end_of_track as u64));
513            o.insert("end_group".into(), vi(m.end_group.into_inner()));
514            o.insert("end_object".into(), vi(m.end_object.into_inner()));
515            o.insert("parameters".into(), params_to_json(&m.parameters));
516            o.insert("track_properties".into(), track_props_to_json(&m.track_properties));
517            o
518        }
519        ControlMessage::PublishSkipped(m) => {
520            let mut o = Map::new();
521            o.insert("namespace_suffix".into(), ns_to_json(&m.namespace_suffix));
522            o.insert(
523                "track_name".into(),
524                Value::Text(String::from_utf8_lossy(&m.track_name).into_owned()),
525            );
526            o
527        }
528    };
529    obj
530}