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}