Skip to main content

moqtap_trace/
header.rs

1use std::collections::BTreeMap;
2
3use ciborium::Value;
4
5use crate::error::MoqTraceError;
6
7// ── map lookup helpers ─────────────────────────────────────
8
9fn find<'a>(pairs: &'a [(Value, Value)], key: &str) -> Option<&'a Value> {
10    pairs.iter().find(|(k, _)| k.as_text() == Some(key)).map(|(_, v)| v)
11}
12
13fn find_text(pairs: &[(Value, Value)], key: &str) -> Option<String> {
14    find(pairs, key).and_then(|v| v.as_text().map(String::from))
15}
16
17fn find_uint(pairs: &[(Value, Value)], key: &str) -> Option<u64> {
18    find(pairs, key).and_then(as_u64)
19}
20
21/// Read an unsigned integer, accepting the float form CBOR also permits.
22///
23/// An encoder may write an integral value as a float, and one of the two
24/// implementations of this format does exactly that for anything past 32 bits
25/// — which every epoch-millisecond timestamp is. Reading only major type 0
26/// here meant `startTime` went missing from every trace that encoder wrote,
27/// and the file was rejected before a single event was read.
28///
29/// A float carries integers exactly only up to 2^53, so one that is
30/// fractional or beyond that range is not an integer this can honour, and is
31/// refused rather than rounded.
32pub(crate) fn as_u64(value: &Value) -> Option<u64> {
33    match value {
34        Value::Integer(i) => u64::try_from(*i).ok(),
35        Value::Float(f) => {
36            let f = *f;
37            if f.fract() == 0.0 && f >= 0.0 && f <= (2u64 << 52) as f64 {
38                Some(f as u64)
39            } else {
40                None
41            }
42        }
43        _ => None,
44    }
45}
46
47/// Read a signed integer, accepting the float form. See [`as_u64`].
48pub(crate) fn as_i64(value: &Value) -> Option<i64> {
49    match value {
50        Value::Integer(i) => i64::try_from(*i).ok(),
51        Value::Float(f) => {
52            let f = *f;
53            let limit = (2u64 << 52) as f64;
54            if f.fract() == 0.0 && f >= -limit && f <= limit {
55                Some(f as i64)
56            } else {
57                None
58            }
59        }
60        _ => None,
61    }
62}
63
64/// Write a number as a CBOR integer when its value is integral, and as a float
65/// only when it is not.
66///
67/// The format's encoding rule (SPEC.md, Interoperability) requires an integral
68/// value to be written as a CBOR integer rather than a float, and is about the
69/// *value* rather than the type the document gives the key.
70/// `"effectiveRate"` is the one
71/// key here declared a float, and its commonest value is `1.0` — "no
72/// rate-based dropping" — so writing it as a float meant the two
73/// implementations emitted different major types for the same trace, in the
74/// case that occurs most. Both readers accept either form; the bytes still
75/// differed, which is exactly what the rule exists to stop.
76///
77/// A value with a fractional part, one past the range a float carries integers
78/// exactly, and NaN or an infinity (whose `fract()` is NaN) all stay floats.
79fn number_value(x: f64) -> Value {
80    if x.fract() == 0.0 && x.abs() <= (2u64 << 52) as f64 {
81        Value::Integer((x as i64).into())
82    } else {
83        Value::Float(x)
84    }
85}
86
87fn find_bool(pairs: &[(Value, Value)], key: &str) -> Option<bool> {
88    match find(pairs, key) {
89        Some(Value::Bool(b)) => Some(*b),
90        _ => None,
91    }
92}
93
94/// Read a required text key, distinguishing "absent" from "present and not
95/// text".
96///
97/// Both are malformed — there is no header to construct without the key — but
98/// they are different faults, and reporting a `"perspective": 5` as a missing
99/// `"perspective"` sends whoever has to fix the file looking for something
100/// that is right in front of them. `label` names the key as a reader of the
101/// file would find it, which for a key inside `"segment"` is not its bare
102/// name.
103fn required_text(
104    pairs: &[(Value, Value)],
105    key: &str,
106    label: &str,
107) -> Result<String, MoqTraceError> {
108    match find(pairs, key) {
109        Some(v) => v
110            .as_text()
111            .map(String::from)
112            .ok_or_else(|| MoqTraceError::InvalidHeader(format!("'{label}' is not a text string"))),
113        None => Err(MoqTraceError::InvalidHeader(format!("missing '{label}'"))),
114    }
115}
116
117/// Read a required unsigned-integer key. See [`required_text`].
118fn required_uint(pairs: &[(Value, Value)], key: &str, label: &str) -> Result<u64, MoqTraceError> {
119    match find(pairs, key) {
120        Some(v) => as_u64(v).ok_or_else(|| {
121            MoqTraceError::InvalidHeader(format!("'{label}' is not an unsigned integer"))
122        }),
123        None => Err(MoqTraceError::InvalidHeader(format!("missing '{label}'"))),
124    }
125}
126
127// ── unrecognised-key stores ────────────────────────────────
128
129/// Whether `key` belongs in a map's unrecognised-key store rather than in one
130/// of the map's own fields, given that map's `writes_key`.
131///
132/// This is the writing half's question: an entry naming a key the map already
133/// writes from a field is not written a second time from the store, since a
134/// CBOR map carrying one key twice is a map no two readers need agree on. The
135/// reading half asks a finer one — see [`unrecognised`], which has to tell two
136/// entries with the same key apart.
137///
138/// A non-text key is unrecognised by construction: every key this format
139/// defines is text.
140fn goes_to_store(key: &Value, writes_key: impl Fn(&str) -> bool) -> bool {
141    !key.as_text().is_some_and(writes_key)
142}
143
144/// Rewrite a value into the encoding this format requires of a writer, leaving
145/// what it says alone.
146///
147/// The two normative encoding rules — an integral value is written as a CBOR
148/// integer rather than a float, and a byte string as major type 2 rather than
149/// under RFC 8746's tag 64 — bind the bytes a *writer* emits, so they bind
150/// every value it emits and not only the ones it understood. A store holds
151/// values this crate never looked at, and the JavaScript implementation's
152/// decoder folds both shapes away before its own code sees them: it cannot
153/// emit either, whatever its store holds. A Rust writer that re-emitted them
154/// would produce different bytes for the same input file, which is the one
155/// thing those two rules exist to stop. SPEC.md extends both rules to stored
156/// values for exactly that reason — see its Interoperability section, under
157/// the shapes a CBOR library may normalise before a reader sees them.
158///
159/// Not the header's alone: an event carries opaque values too — a control
160/// message's `"msg"`, an annotation's `"data"`, an unknown event type's
161/// fields and [`TraceEvent::extra`](crate::event::TraceEvent::extra) — and
162/// they are written through this same function. Two copies of this rule that
163/// had to agree would be the defect it exists to fix.
164///
165/// Applied on write and never on read: a value read into a store still
166/// compares equal to what the file carried, and the house style is the
167/// serialiser's business. Recursive, because a stored value may be a whole
168/// tree and the rules are about every number and every byte string in it, and
169/// applied to map *keys* as well as values, which are encoded by the same
170/// rules as anything else.
171///
172/// One value changes rather than merely changing encoding: `-0.0` written as
173/// `0` loses its sign. SPEC.md calls that out and accepts it — no field in a
174/// trace gives negative zero a meaning — so there is deliberately no exception
175/// for it here.
176///
177/// A map inside a stored value is deduplicated on the same terms as the store
178/// itself: SPEC.md forbids a conformant tool from emitting a map with a
179/// repeated key even having read one, and that binds every map a writer emits
180/// rather than only the outermost. Reading such a map still hands back both
181/// entries — that half is observable here and must be preserved — and writing
182/// it emits the first.
183pub(crate) fn normalised(value: &Value) -> Value {
184    match value {
185        // SPEC.md's first encoding rule: an integral value goes out as a
186        // CBOR integer, not as a float. The same function the header's own
187        // float-typed key is written through.
188        Value::Float(f) => number_value(*f),
189        // SPEC.md's second encoding rule: a byte string goes out as major
190        // type 2, never wrapped in RFC 8746's typed-array tag 64. That tag
191        // is `uint8 array`, whose
192        // content is a byte string and whose meaning is that byte string; the
193        // tag records only that some language held it as a typed array.
194        Value::Tag(64, inner) if matches!(**inner, Value::Bytes(_)) => (**inner).clone(),
195        Value::Tag(tag, inner) => Value::Tag(*tag, Box::new(normalised(inner))),
196        Value::Array(items) => Value::Array(items.iter().map(normalised).collect()),
197        Value::Map(pairs) => {
198            Value::Map(deduplicated(pairs.iter().map(|(k, v)| (normalised(k), normalised(v)))))
199        }
200        // Integers, byte strings, text, booleans and null are already what the
201        // rules ask for. `Value` is `#[non_exhaustive]`, so anything ciborium
202        // adds later arrives here and is passed through untouched, which is
203        // the safe direction: it is preserved rather than mangled.
204        other => other.clone(),
205    }
206}
207
208/// Whether two keys about to be written are one key in the file.
209///
210/// [`Value`]'s equality is CBOR's everywhere but on NaN, where it follows
211/// IEEE 754 and answers `false` for a value compared with itself. Two NaN keys
212/// encode to the same bytes, so a map carrying both is as invalid as any other
213/// map with a repeated key, and equality alone would wave exactly that one
214/// case through.
215fn same_key(one: &Value, other: &Value) -> bool {
216    match (one, other) {
217        (Value::Float(a), Value::Float(b)) if a.is_nan() && b.is_nan() => true,
218        _ => one == other,
219    }
220}
221
222/// The entries of a map to be written, with no key twice: the first entry for
223/// a key is kept in its place and the rest are dropped.
224///
225/// Callers pass entries that are already [`normalised`], because normalisation
226/// can *create* a collision — `Float(1.0)` and `Integer(1)` are two keys in a
227/// store and one key in a file, as are a byte string and the same bytes under
228/// tag 64 — so a check that ran first would miss it.
229///
230/// **First wins, not last.** Every read in this file goes through [`find`],
231/// which returns the first entry for a key, so keeping the first is what makes
232/// the entry a caller is shown the entry that survives a rewrite; keeping the
233/// last would hand back one value and file another. It also makes the rewrite
234/// a fixed point rather than something a value can drift across.
235///
236/// Quadratic, and deliberately: these are the keys of one map, the comparison
237/// is a `Value` equality rather than a hash of an arbitrary tree, and `Value`
238/// is not `Hash` at all — a CBOR value may be a float.
239fn deduplicated(entries: impl Iterator<Item = (Value, Value)>) -> Vec<(Value, Value)> {
240    let mut kept: Vec<(Value, Value)> = Vec::new();
241    for (key, value) in entries {
242        if kept.iter().any(|(already, _)| same_key(already, &key)) {
243            continue;
244        }
245        kept.push((key, value));
246    }
247    kept
248}
249
250/// Every entry of `pairs` the decoded map does not write back out of a field
251/// of its own: a key this crate has never heard of, a key it knows whose value
252/// it could not use — and a second entry for a key whose *first* entry a field
253/// did take.
254///
255/// That last case is why this walks entries rather than filtering on key
256/// names. Every field is read through [`find`], which returns the first entry
257/// for its key, so a header carrying `"transport": "wt"` and then
258/// `"transport": 42` hands `"wt"` to the field and leaves the `42`
259/// unaccounted for. Filtering the store by key name dropped that entry along
260/// with the one the field was holding, and a value the file carried reached
261/// neither the field nor the store: reading the file deleted it outright.
262/// RFC 8949 makes such a map invalid and SPEC.md leaves readers free to
263/// disagree over which entry of a duplicate pair wins, but neither licenses
264/// losing the other one on the way in.
265///
266/// The kept entry is not written back — [`store_entries`] drops it, because
267/// the field writes that key and a map may not carry it twice — so a rewrite
268/// of a duplicate-keyed map emits one entry of the pair, which is the most a
269/// conformant writer can emit, and is a fixed point from there.
270///
271/// Events use this too, with their own `writes_key`. Which entry a field
272/// actually took is the caller's business, not this walk's: a lookup that
273/// returns the first entry for a key whatever it holds leaves the first, and
274/// one that skips an entry whose value it cannot use may leave a later one.
275/// This drops the first entry for a key the map writes either way, so on a
276/// mixed-type duplicate the two disagree about *which* entry survives. Both
277/// were lost before this walk existed, and SPEC.md leaves readers free to
278/// disagree over which of a duplicate pair wins, so nothing may depend on it.
279pub(crate) fn unrecognised(
280    pairs: &[(Value, Value)],
281    writes_key: impl Fn(&str) -> bool,
282) -> Vec<(Value, Value)> {
283    let mut taken: Vec<&str> = Vec::new();
284    let mut store: Vec<(Value, Value)> = Vec::new();
285    for (key, value) in pairs {
286        if let Some(name) = key.as_text() {
287            // The first entry for a key the map writes is the entry the field
288            // is holding: `find` returned this one. Every later entry for that
289            // key is a value no field took, and goes to the store.
290            if writes_key(name) && !taken.contains(&name) {
291                taken.push(name);
292                continue;
293            }
294        }
295        store.push((key.clone(), value.clone()));
296    }
297    store
298}
299
300/// Map entries a writer is about to emit out of pairs that came from a file:
301/// [`normalised`] into the encoding SPEC.md requires, and with no key written
302/// twice ([`deduplicated`]).
303///
304/// Normalising first is deliberate — it can *create* a collision, so a dedup
305/// pass that ran before it would miss one. See [`deduplicated`].
306///
307/// A list of pairs is not a map and can hold one key twice: a caller can build
308/// such a list by hand, and a file that repeated a key no field could use
309/// leaves one behind (see [`unrecognised`]). Until this pass existed both
310/// entries went into the file. RFC 8949 calls a map with a repeated key
311/// invalid, the JavaScript reader silently collapses one, and SPEC.md forbids
312/// emitting one at all.
313///
314/// Used for every such list this crate writes: the header's three stores
315/// through [`store_entries`], and on the event side
316/// [`TraceEvent::extra`](crate::event::TraceEvent::extra) and an unknown event
317/// type's fields. `"custom"` needs only the [`normalised`] half — a
318/// `BTreeMap` cannot hold one key twice, so the dedup pass has nothing to
319/// find there.
320pub(crate) fn written_entries<'a>(
321    entries: impl Iterator<Item = &'a (Value, Value)>,
322) -> Vec<(Value, Value)> {
323    deduplicated(entries.map(|(key, value)| (normalised(key), normalised(value))))
324}
325
326/// The store entries to write after a map's own keys.
327///
328/// [`written_entries`] with one pass in front of it: an entry naming a key the
329/// map writes from a field is dropped, because the field wins and writing both
330/// would put that key in the map twice.
331pub(crate) fn store_entries(
332    extra: &[(Value, Value)],
333    writes_key: impl Fn(&str) -> bool,
334) -> Vec<(Value, Value)> {
335    written_entries(extra.iter().filter(|(key, _)| goes_to_store(key, &writes_key)))
336}
337
338/// Recording perspective — who captured the trace.
339///
340/// An unrecognised value is preserved as [`Perspective::Other`] rather than
341/// rejected: the format admits new perspectives without a version bump, and
342/// every event in such a file is still parseable.
343#[derive(Debug, Clone, PartialEq, Eq)]
344#[non_exhaustive]
345pub enum Perspective {
346    /// MoQT client (initiator of the QUIC connection).
347    Client,
348    /// MoQT server or relay (single-session view from inside the endpoint).
349    Server,
350    /// Passive observer (e.g. DevTools extension, network tap).
351    Observer,
352    /// Active capture from inside a relay, reporting on multiple concurrent
353    /// peer sessions. Distinct from [`Perspective::Server`] because events
354    /// span peers and carry a peer identifier.
355    RelayTap,
356    /// A perspective this version of the crate does not know, kept verbatim.
357    Other(String),
358}
359
360impl Perspective {
361    /// The wire spelling of this perspective.
362    pub fn as_str(&self) -> &str {
363        match self {
364            Perspective::Client => "client",
365            Perspective::Server => "server",
366            Perspective::Observer => "observer",
367            Perspective::RelayTap => "relay-tap",
368            Perspective::Other(s) => s,
369        }
370    }
371
372    fn parse(s: &str) -> Self {
373        match s {
374            "client" => Perspective::Client,
375            "server" => Perspective::Server,
376            "observer" => Perspective::Observer,
377            "relay-tap" => Perspective::RelayTap,
378            other => Perspective::Other(other.to_string()),
379        }
380    }
381}
382
383impl std::fmt::Display for Perspective {
384    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
385        f.write_str(self.as_str())
386    }
387}
388
389/// Detail level — declares what was recorded.
390///
391/// Each known level is a strict superset of the one above it, which is what
392/// the [`Ord`] impl orders by. [`DetailLevel::Other`] sorts above `Full`
393/// deliberately: a level this crate does not know might reveal anything, and
394/// a privacy check written as `detail >= HeadersData` should err towards
395/// warning rather than towards silence.
396#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
397#[non_exhaustive]
398pub enum DetailLevel {
399    /// Control messages only.
400    Control,
401    /// Control messages + data stream headers and object metadata.
402    Headers,
403    /// Headers + payload byte lengths.
404    HeadersSizes,
405    /// Headers + full payload bytes.
406    HeadersData,
407    /// Everything above + raw wire bytes.
408    Full,
409    /// A level this version of the crate does not know, kept verbatim.
410    Other(String),
411}
412
413impl DetailLevel {
414    /// The wire spelling of this detail level.
415    pub fn as_str(&self) -> &str {
416        match self {
417            DetailLevel::Control => "control",
418            DetailLevel::Headers => "headers",
419            DetailLevel::HeadersSizes => "headers+sizes",
420            DetailLevel::HeadersData => "headers+data",
421            DetailLevel::Full => "full",
422            DetailLevel::Other(s) => s,
423        }
424    }
425
426    fn parse(s: &str) -> Self {
427        match s {
428            "control" => DetailLevel::Control,
429            "headers" => DetailLevel::Headers,
430            "headers+sizes" => DetailLevel::HeadersSizes,
431            "headers+data" => DetailLevel::HeadersData,
432            "full" => DetailLevel::Full,
433            other => DetailLevel::Other(other.to_string()),
434        }
435    }
436}
437
438impl std::fmt::Display for DetailLevel {
439    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
440        f.write_str(self.as_str())
441    }
442}
443
444/// Drop policy applied when a sampled source could not keep up.
445#[derive(Debug, Clone, PartialEq, Eq)]
446#[non_exhaustive]
447pub enum DropPolicy {
448    /// Drop the oldest events first.
449    Head,
450    /// Drop the newest events first.
451    Tail,
452    /// Random sampling at the configured rate.
453    Sampled,
454    /// A policy this version of the crate does not know, kept verbatim.
455    Other(String),
456}
457
458impl DropPolicy {
459    /// The wire spelling of this drop policy.
460    pub fn as_str(&self) -> &str {
461        match self {
462            DropPolicy::Head => "head",
463            DropPolicy::Tail => "tail",
464            DropPolicy::Sampled => "sampled",
465            DropPolicy::Other(s) => s,
466        }
467    }
468
469    fn parse(s: &str) -> Self {
470        match s {
471            "head" => DropPolicy::Head,
472            "tail" => DropPolicy::Tail,
473            "sampled" => DropPolicy::Sampled,
474            other => DropPolicy::Other(other.to_string()),
475        }
476    }
477}
478
479impl std::fmt::Display for DropPolicy {
480    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
481        f.write_str(self.as_str())
482    }
483}
484
485/// Per-segment metadata.
486///
487/// Present only in segmented traces. A single-shot file must not carry it —
488/// its absence is what tells a reader that event sequence numbers and
489/// timestamps are file-global rather than segment-local.
490// No `Eq`: `extra` holds arbitrary CBOR, and a CBOR value may be a float, for
491// which equality is not reflexive. `TraceHeader` and `SamplingInfo` have never
492// had it for the same reason.
493#[derive(Debug, Clone, PartialEq)]
494pub struct SegmentInfo {
495    /// 0-based sequence number of this segment within the stream.
496    pub sequence: u64,
497    /// Nominal segment duration in milliseconds. A hint; the actual duration
498    /// may differ.
499    pub duration_ms: Option<u64>,
500    /// Opaque identifier shared by every segment of the same logical stream.
501    pub stream_id: Option<String>,
502    /// `true` if this segment continues a previous one with the same
503    /// `stream_id`. `false` or absent for the first segment.
504    pub continues: Option<bool>,
505    /// Keys in the `"segment"` map that this version of the crate could not
506    /// use, kept verbatim and written back into that map.
507    ///
508    /// The store belongs to `"segment"` and not to the header: a private key
509    /// on the segment and a key of the same name at the top level are
510    /// different keys, and re-emitting either in the other's map changes what
511    /// the file says. See [`TraceHeader::extra`].
512    ///
513    /// Empty for every segment this crate constructs itself.
514    pub extra: Vec<(Value, Value)>,
515}
516
517impl SegmentInfo {
518    /// A segment carrying only its sequence number.
519    pub fn new(sequence: u64) -> Self {
520        SegmentInfo {
521            sequence,
522            duration_ms: None,
523            stream_id: None,
524            continues: None,
525            extra: Vec::new(),
526        }
527    }
528
529    /// Whether `key` is written from one of this map's own fields —
530    /// equivalently, on a segment that was decoded, whether the decode used
531    /// it. See [`TraceHeader::writes_key`] for why the destructuring is
532    /// exhaustive and why this is not "does the format define `key`".
533    fn writes_key(&self, key: &str) -> bool {
534        let SegmentInfo { sequence: _, duration_ms, stream_id, continues, extra: _ } = self;
535        key == "sequence"
536            || (key == "durationMs" && duration_ms.is_some())
537            || (key == "streamId" && stream_id.is_some())
538            || (key == "continues" && continues.is_some())
539    }
540
541    fn to_value(&self) -> Value {
542        let mut pairs: Vec<(Value, Value)> =
543            vec![(Value::Text("sequence".into()), Value::Integer(self.sequence.into()))];
544        if let Some(d) = self.duration_ms {
545            pairs.push((Value::Text("durationMs".into()), Value::Integer(d.into())));
546        }
547        if let Some(ref s) = self.stream_id {
548            pairs.push((Value::Text("streamId".into()), Value::Text(s.clone())));
549        }
550        if let Some(c) = self.continues {
551            pairs.push((Value::Text("continues".into()), Value::Bool(c)));
552        }
553        // Last, so the segment's own keys keep the positions a reader expects.
554        pairs.extend(store_entries(&self.extra, |k| self.writes_key(k)));
555        Value::Map(pairs)
556    }
557
558    /// Decode the contents of a `"segment"` map.
559    ///
560    /// The caller has already established that the value is a map: a
561    /// `"segment"` that is not one is an unusable optional value on the
562    /// *header*, and goes to the header's store rather than failing the file.
563    fn from_pairs(pairs: &[(Value, Value)]) -> Result<Self, MoqTraceError> {
564        // `"sequence"` is the one key in the header whose absence or
565        // unusability makes the header malformed: it is the sole ordering key
566        // of a segmented stream, and a default would invent an order the file
567        // never had.
568        let mut info = SegmentInfo {
569            sequence: required_uint(pairs, "sequence", "segment.sequence")?,
570            duration_ms: find_uint(pairs, "durationMs"),
571            stream_id: find_text(pairs, "streamId"),
572            continues: find_bool(pairs, "continues"),
573            extra: Vec::new(),
574        };
575        let extra = unrecognised(pairs, |k| info.writes_key(k));
576        info.extra = extra;
577        Ok(info)
578    }
579}
580
581/// Sampling and filtering metadata. Present only when events were dropped or
582/// filtered at the source.
583///
584/// Its absence means the trace is complete relative to the declared detail
585/// level. Sources must not drop control messages, state changes, errors, or
586/// peer and subscription events under any policy — those carry the causal
587/// structure that makes the rest of the trace readable — so a source that
588/// cannot keep up with them refuses the recording instead. When sampling is
589/// active, `applies_to` names the event types the policy actually touched,
590/// and a reader may treat every other type as complete.
591#[derive(Debug, Clone, Default, PartialEq)]
592pub struct SamplingInfo {
593    /// Effective fraction of source events retained, in `(0.0, 1.0]`.
594    pub effective_rate: Option<f64>,
595    /// Per-segment cap that triggered drops, if rate-limited.
596    pub max_events_per_sec: Option<u64>,
597    /// Drop strategy applied when the cap was exceeded.
598    pub drop_policy: Option<DropPolicy>,
599    /// Cumulative events dropped since `start_time`. In a segmented trace this
600    /// is the running total at the start of this segment.
601    pub dropped_total: Option<u64>,
602    /// Events dropped within this segment only.
603    pub dropped_segment: Option<u64>,
604    /// Source-side filter rule that selected events.
605    pub rule: Option<String>,
606    /// Filter language the rule is written in (`"prefix"`, `"glob"`, `"cel"`).
607    pub rule_lang: Option<String>,
608    /// Event type IDs the drop policy was applied to.
609    ///
610    /// `None` when the file carried no `"appliesTo"` — and also when it
611    /// carried one this crate could not read *whole*, in which case the array
612    /// is kept verbatim in [`SamplingInfo::extra`]. An `"appliesTo"` of
613    /// `[3, "x", 5]` read as `[3, 5]` would not be a partial answer: the key
614    /// names the event types the drop policy touched, and a reader may treat
615    /// every type absent from it as complete, so shortening the array reports
616    /// a sampled event type as fully recorded.
617    pub applies_to: Option<Vec<u64>>,
618    /// Keys in the `"sampling"` map that this version of the crate could not
619    /// use, kept verbatim and written back into that map. See
620    /// [`TraceHeader::extra`].
621    ///
622    /// Empty for every sampling map this crate constructs itself.
623    pub extra: Vec<(Value, Value)>,
624}
625
626impl SamplingInfo {
627    /// Whether `key` is written from one of this map's own fields. See
628    /// [`TraceHeader::writes_key`].
629    fn writes_key(&self, key: &str) -> bool {
630        let SamplingInfo {
631            effective_rate,
632            max_events_per_sec,
633            drop_policy,
634            dropped_total,
635            dropped_segment,
636            rule,
637            rule_lang,
638            applies_to,
639            extra: _,
640        } = self;
641        (key == "effectiveRate" && effective_rate.is_some())
642            || (key == "maxEventsPerSec" && max_events_per_sec.is_some())
643            || (key == "dropPolicy" && drop_policy.is_some())
644            || (key == "droppedTotal" && dropped_total.is_some())
645            || (key == "droppedSegment" && dropped_segment.is_some())
646            || (key == "rule" && rule.is_some())
647            || (key == "ruleLang" && rule_lang.is_some())
648            || (key == "appliesTo" && applies_to.is_some())
649    }
650
651    fn to_value(&self) -> Value {
652        let mut pairs: Vec<(Value, Value)> = Vec::new();
653        if let Some(r) = self.effective_rate {
654            pairs.push((Value::Text("effectiveRate".into()), number_value(r)));
655        }
656        if let Some(m) = self.max_events_per_sec {
657            pairs.push((Value::Text("maxEventsPerSec".into()), Value::Integer(m.into())));
658        }
659        if let Some(ref p) = self.drop_policy {
660            pairs.push((Value::Text("dropPolicy".into()), Value::Text(p.as_str().into())));
661        }
662        if let Some(d) = self.dropped_total {
663            pairs.push((Value::Text("droppedTotal".into()), Value::Integer(d.into())));
664        }
665        if let Some(d) = self.dropped_segment {
666            pairs.push((Value::Text("droppedSegment".into()), Value::Integer(d.into())));
667        }
668        if let Some(ref r) = self.rule {
669            pairs.push((Value::Text("rule".into()), Value::Text(r.clone())));
670        }
671        if let Some(ref r) = self.rule_lang {
672            pairs.push((Value::Text("ruleLang".into()), Value::Text(r.clone())));
673        }
674        if let Some(ref a) = self.applies_to {
675            let arr: Vec<Value> = a.iter().map(|&i| Value::Integer(i.into())).collect();
676            pairs.push((Value::Text("appliesTo".into()), Value::Array(arr)));
677        }
678        // Last, so the map's own keys keep the positions a reader expects.
679        pairs.extend(store_entries(&self.extra, |k| self.writes_key(k)));
680        Value::Map(pairs)
681    }
682
683    /// Decode the contents of a `"sampling"` map. Infallible: the map has no
684    /// required keys, so every value this cannot use goes to the store.
685    ///
686    /// The caller has already established that the value is a map; a
687    /// `"sampling"` that is not one goes to the header's store.
688    fn from_pairs(pairs: &[(Value, Value)]) -> Self {
689        let effective_rate = find(pairs, "effectiveRate")
690            .and_then(|v| match v {
691                Value::Float(f) => Some(*f),
692                Value::Integer(i) => i64::try_from(*i).ok().map(|n| n as f64),
693                _ => None,
694            })
695            // The key is defined as a fraction in `(0.0, 1.0]`, and a value
696            // outside the range its meaning allows is unusable on an optional
697            // key. NaN fails this comparison too, which is the wanted answer:
698            // there is no rate to report and the bytes go to the store.
699            .filter(|r| *r > 0.0 && *r <= 1.0);
700        let applies_to = match find(pairs, "appliesTo") {
701            // All or nothing: one element that is not an event type ID makes
702            // the array unusable entire, and it goes to the store from there.
703            Some(Value::Array(items)) => items.iter().map(as_u64).collect::<Option<Vec<u64>>>(),
704            _ => None,
705        };
706        let mut info = SamplingInfo {
707            effective_rate,
708            max_events_per_sec: find_uint(pairs, "maxEventsPerSec"),
709            drop_policy: find_text(pairs, "dropPolicy").as_deref().map(DropPolicy::parse),
710            dropped_total: find_uint(pairs, "droppedTotal"),
711            dropped_segment: find_uint(pairs, "droppedSegment"),
712            rule: find_text(pairs, "rule"),
713            rule_lang: find_text(pairs, "ruleLang"),
714            applies_to,
715            extra: Vec::new(),
716        };
717        let extra = unrecognised(pairs, |k| info.writes_key(k));
718        info.extra = extra;
719        info
720    }
721}
722
723/// Session metadata written at the start of a `.moqtrace` file, and at the
724/// start of every segment in a segmented one.
725#[derive(Debug, Clone, PartialEq)]
726pub struct TraceHeader {
727    /// MoQT version identifier (e.g. `"moq-transport-17"`, `"moq-transport-rfc9999"`).
728    pub protocol: String,
729    /// Recording viewpoint.
730    pub perspective: Perspective,
731    /// Detail level.
732    pub detail: DetailLevel,
733    /// Recording start time (Unix epoch milliseconds). In a segmented trace
734    /// this is the segment's start, not the stream's.
735    pub start_time: u64,
736    /// Recording end time (Unix epoch milliseconds). Set when the trace is
737    /// finalized, so a crash-truncated file has none.
738    pub end_time: Option<u64>,
739    /// Transport type (e.g. `"webtransport"`, `"raw-quic"`).
740    pub transport: Option<String>,
741    /// Software that produced the trace. Also namespaces the source-local
742    /// peer identifiers carried on events.
743    pub source: Option<String>,
744    /// Remote peer URI.
745    pub endpoint: Option<String>,
746    /// Capture-correlation identifier, grouping traces of one logical session
747    /// recorded from different vantage points.
748    pub session_id: Option<String>,
749    /// Per-segment metadata. Present only when this header begins one segment
750    /// of a segmented stream.
751    pub segment: Option<SegmentInfo>,
752    /// Sampling and filter metadata. Present only when events were dropped or
753    /// filtered at the source.
754    pub sampling: Option<SamplingInfo>,
755    /// User-defined metadata. `"payloadMasked": true` here declares that
756    /// payload bytes were zeroed before writing.
757    ///
758    /// `None` when the file carried no `"custom"` — and also when it carried
759    /// one this map cannot hold exactly: a `"custom"` that is not a map, or
760    /// one with a key that is not text, in which case the whole value is kept
761    /// verbatim in [`TraceHeader::extra`]. Losing typed access is the smaller
762    /// harm; nothing in the format gives `"custom"` keys meaning, so there is
763    /// nothing to lose but convenience, and the bytes survive.
764    ///
765    /// A `"custom"` carrying one key twice takes the same route, a `BTreeMap`
766    /// being unable to hold it either. That one is kept whole on the way in
767    /// and written with the repeat dropped: a writer may not emit a map with a
768    /// repeated key, so a rewrite keeps the first entry rather than both. See
769    /// [`TraceHeader::extra`].
770    ///
771    /// `"custom"` has no store of its own. Every key in it belongs to whoever
772    /// wrote the trace, so there is no such thing as an unrecognised key
773    /// there: it is a passthrough, handed back key for key and written back
774    /// as it was handed over — the value, not its encoding, which the two
775    /// rules in [`TraceHeader::extra`] apply to here as well.
776    pub custom: Option<BTreeMap<String, Value>>,
777    /// Keys in the header map that this version of the crate could not use,
778    /// kept verbatim.
779    ///
780    /// A key the format does not define lands here, and so does a key it
781    /// *does* define carrying a value this crate cannot use — `"transport":
782    /// 42`, an `"endTime"` with a fractional part, a `"segment"` that is not a
783    /// map. Knowing more about a key must not mean preserving it less: the
784    /// value is ignored for meaning, the field that would have held it reads
785    /// `None`, and the entry is written back unchanged after the header's own
786    /// keys.
787    ///
788    /// Dropping such a key instead would emit a valid file that looks as
789    /// though it never carried it, and the tools that read a trace and write
790    /// it back — a redaction pass, a filter, a re-segmentation — are exactly
791    /// the ones a trace passes through on its way to someone else.
792    ///
793    /// "Unchanged" binds the value and not its encoding. On the way out, an
794    /// integral float in a stored value is written as a CBOR integer and a
795    /// byte string under RFC 8746's tag 64 as major type 2, at any depth,
796    /// because SPEC.md's two encoding rules are about every byte this crate
797    /// emits rather than only the keys it understood — the JavaScript
798    /// implementation's decoder folds both shapes away before its own code
799    /// sees them, so it could not emit either from a store however hard it
800    /// tried. Nothing a comparison of the two values can see changes, with the
801    /// single exception SPEC.md names: `-0.0` written as `0` loses its sign.
802    ///
803    /// A CBOR map may not carry one key twice, and this list can: it is an
804    /// ordered list of pairs, not a map, whether it was populated by hand or
805    /// from a file whose header repeated a key. So on the way out an entry
806    /// naming a key the header writes from a field is dropped, and of two
807    /// entries sharing a key the first is written — as it is for a map nested
808    /// inside a stored value, which is a map this crate emits too.
809    ///
810    /// This is the header map's store only. [`SegmentInfo::extra`] and
811    /// [`SamplingInfo::extra`] keep their own, because a private key on
812    /// `"segment"` and a key of the same name at the top level are different
813    /// keys.
814    ///
815    /// Empty for every header this crate constructs itself.
816    pub extra: Vec<(Value, Value)>,
817}
818
819impl TraceHeader {
820    /// A header carrying only the four required fields.
821    ///
822    /// Prefer this over a struct literal and assign the optional fields you
823    /// need: a literal has to be edited every time the format gains a field,
824    /// this does not.
825    pub fn new(
826        protocol: impl Into<String>,
827        perspective: Perspective,
828        detail: DetailLevel,
829        start_time: u64,
830    ) -> Self {
831        TraceHeader {
832            protocol: protocol.into(),
833            perspective,
834            detail,
835            start_time,
836            end_time: None,
837            transport: None,
838            source: None,
839            endpoint: None,
840            session_id: None,
841            segment: None,
842            sampling: None,
843            custom: None,
844            extra: Vec::new(),
845        }
846    }
847
848    /// Whether `key` is written from one of the header map's own fields —
849    /// equivalently, on a header that was decoded, whether the decode used it.
850    /// The `"segment"` and `"sampling"` maps answer for their own keys.
851    ///
852    /// Deliberately not "does the format define `key`". The two part company
853    /// on a defined key whose value the decode could not use: such a key is
854    /// treated as unrecognised, so its field stays `None` and the entry goes
855    /// to [`TraceHeader::extra`], from where the encoder writes it back
856    /// unchanged. Asking about the format's whole vocabulary instead would
857    /// keep the key out of `extra` while no field holds it either, and merely
858    /// reading the file would delete the value.
859    ///
860    /// The destructuring is exhaustive on purpose — no `..` — so a new field
861    /// does not compile until it is answered for here. Both ways of getting
862    /// the answer wrong are silent: a field left out has its key written
863    /// twice, once from the field and once from `extra`, and a CBOR map with
864    /// a duplicate key is malformed; a key wrongly claimed is dropped from
865    /// every rewrite.
866    fn writes_key(&self, key: &str) -> bool {
867        let TraceHeader {
868            protocol: _,
869            perspective: _,
870            detail: _,
871            start_time: _,
872            end_time,
873            transport,
874            source,
875            endpoint,
876            session_id,
877            segment,
878            sampling,
879            custom,
880            extra: _,
881        } = self;
882        // The four required keys are written whatever they hold.
883        matches!(key, "protocol" | "perspective" | "detail" | "startTime")
884            || (key == "endTime" && end_time.is_some())
885            || (key == "transport" && transport.is_some())
886            || (key == "source" && source.is_some())
887            || (key == "endpoint" && endpoint.is_some())
888            || (key == "sessionId" && session_id.is_some())
889            || (key == "segment" && segment.is_some())
890            || (key == "sampling" && sampling.is_some())
891            || (key == "custom" && custom.is_some())
892    }
893}
894
895impl From<&TraceHeader> for Value {
896    fn from(h: &TraceHeader) -> Self {
897        let mut pairs: Vec<(Value, Value)> = vec![
898            (Value::Text("protocol".into()), Value::Text(h.protocol.clone())),
899            (Value::Text("perspective".into()), Value::Text(h.perspective.as_str().into())),
900            (Value::Text("detail".into()), Value::Text(h.detail.as_str().into())),
901            (Value::Text("startTime".into()), Value::Integer(h.start_time.into())),
902        ];
903
904        if let Some(end_time) = h.end_time {
905            pairs.push((Value::Text("endTime".into()), Value::Integer(end_time.into())));
906        }
907        if let Some(ref transport) = h.transport {
908            pairs.push((Value::Text("transport".into()), Value::Text(transport.clone())));
909        }
910        if let Some(ref source) = h.source {
911            pairs.push((Value::Text("source".into()), Value::Text(source.clone())));
912        }
913        if let Some(ref endpoint) = h.endpoint {
914            pairs.push((Value::Text("endpoint".into()), Value::Text(endpoint.clone())));
915        }
916        if let Some(ref session_id) = h.session_id {
917            pairs.push((Value::Text("sessionId".into()), Value::Text(session_id.clone())));
918        }
919        if let Some(ref segment) = h.segment {
920            pairs.push((Value::Text("segment".into()), segment.to_value()));
921        }
922        if let Some(ref sampling) = h.sampling {
923            pairs.push((Value::Text("sampling".into()), sampling.to_value()));
924        }
925        if let Some(ref custom) = h.custom {
926            // `"custom"` is a passthrough, but "handed back as it was handed
927            // over" binds the value and not its encoding, exactly as it does
928            // for a store: SPEC.md's two encoding rules are about every byte a
929            // writer emits. The JavaScript implementation cannot emit either
930            // shape from its `custom` either, its decoder having folded both
931            // away, so normalising here is what keeps the two files identical.
932            // A `BTreeMap<String, _>` cannot hold a duplicate or a non-text
933            // key, so there is nothing else to reconcile.
934            let custom_pairs: Vec<(Value, Value)> =
935                custom.iter().map(|(k, v)| (Value::Text(k.clone()), normalised(v))).collect();
936            pairs.push((Value::Text("custom".into()), Value::Map(custom_pairs)));
937        }
938
939        // Last, so the header's own keys keep the positions a reader expects
940        // and the file stays diffable against one written without them. An
941        // entry naming a key the header just wrote from a field is dropped
942        // rather than written twice — a reader never produces one, but a
943        // caller assembling a header by hand can.
944        pairs.extend(store_entries(&h.extra, |k| h.writes_key(k)));
945
946        Value::Map(pairs)
947    }
948}
949
950impl TryFrom<Value> for TraceHeader {
951    type Error = MoqTraceError;
952
953    fn try_from(value: Value) -> Result<Self, MoqTraceError> {
954        let pairs = match value {
955            Value::Map(pairs) => pairs,
956            _ => return Err(MoqTraceError::InvalidHeader("header is not a CBOR map".into())),
957        };
958
959        // The four keys there is no header to construct without. An absent one
960        // and an unusable one are the same fault and reported as one, with a
961        // message that says which it was.
962        let protocol = required_text(&pairs, "protocol", "protocol")?;
963        let perspective = required_text(&pairs, "perspective", "perspective")?;
964        let detail = required_text(&pairs, "detail", "detail")?;
965        let start_time = required_uint(&pairs, "startTime", "startTime")?;
966
967        // `"segment"` and `"sampling"` are optional, and an unusable optional
968        // value must not fail the file: one that is not a map goes to the
969        // header's store below and the reader proceeds as though the key were
970        // absent — which for `"segment"` means reading the trace as
971        // non-segmented. Rejecting the file instead would turn one unreadable
972        // metadata value into the loss of every event behind it.
973        let segment = match find(&pairs, "segment") {
974            Some(Value::Map(segment_pairs)) => Some(SegmentInfo::from_pairs(segment_pairs)?),
975            _ => None,
976        };
977        let sampling = match find(&pairs, "sampling") {
978            Some(Value::Map(sampling_pairs)) => Some(SamplingInfo::from_pairs(sampling_pairs)),
979            _ => None,
980        };
981
982        // `"custom"` is a passthrough with no store of its own, so it is kept
983        // only when this map can hold it exactly: every key text, and no two
984        // keys the same, since a `BTreeMap` would silently collapse those into
985        // one. Anything else is unusable and the whole value goes to the
986        // header's store, rather than handing back a `"custom"` that lost
987        // something through a type that lies to every caller reading it.
988        let custom = match find(&pairs, "custom") {
989            Some(Value::Map(custom_pairs)) => custom_pairs
990                .iter()
991                .map(|(ck, cv)| ck.as_text().map(|s| (s.to_string(), cv.clone())))
992                .collect::<Option<BTreeMap<String, Value>>>()
993                .filter(|map| map.len() == custom_pairs.len()),
994            _ => None,
995        };
996
997        let mut header = TraceHeader {
998            protocol,
999            perspective: Perspective::parse(&perspective),
1000            detail: DetailLevel::parse(&detail),
1001            start_time,
1002            end_time: find_uint(&pairs, "endTime"),
1003            transport: find_text(&pairs, "transport"),
1004            source: find_text(&pairs, "source"),
1005            endpoint: find_text(&pairs, "endpoint"),
1006            session_id: find_text(&pairs, "sessionId"),
1007            segment,
1008            sampling,
1009            custom,
1010            extra: Vec::new(),
1011        };
1012        // Decided by what the decode above actually consumed, never by a list
1013        // of key names kept alongside it: the two differ exactly on a defined
1014        // key carrying a value no field could take, and that gap is where a
1015        // value gets silently deleted.
1016        let extra = unrecognised(&pairs, |k| header.writes_key(k));
1017        header.extra = extra;
1018        Ok(header)
1019    }
1020}