moqtap_trace/event.rs
1use ciborium::Value;
2
3use crate::error::MoqTraceError;
4use crate::header::{as_i64, as_u64, normalised, store_entries, unrecognised, DetailLevel};
5
6/// Direction of a message or stream relative to the recording endpoint.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Direction {
9 /// Sent (outgoing). Wire value: `0`.
10 Send,
11 /// Received (incoming). Wire value: `1`.
12 Receive,
13}
14
15impl Direction {
16 fn from_cbor(v: &Value) -> Result<Self, MoqTraceError> {
17 match as_u64(v) {
18 Some(0) => Ok(Direction::Send),
19 Some(1) => Ok(Direction::Receive),
20 _ => Err(MoqTraceError::InvalidEvent("invalid direction value".into())),
21 }
22 }
23}
24
25/// Data stream type.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum StreamType {
28 /// Subgroup stream. Wire value: `0`.
29 Subgroup,
30 /// Datagram. Wire value: `1`.
31 Datagram,
32 /// Fetch stream. Wire value: `2`.
33 Fetch,
34}
35
36impl StreamType {
37 fn from_cbor(v: &Value) -> Result<Self, MoqTraceError> {
38 match as_u64(v) {
39 Some(0) => Ok(StreamType::Subgroup),
40 Some(1) => Ok(StreamType::Datagram),
41 Some(2) => Ok(StreamType::Fetch),
42 _ => Err(MoqTraceError::InvalidEvent("invalid stream type value".into())),
43 }
44 }
45}
46
47/// What sort of failure an [`Error`](EventData::Error) event records.
48///
49/// An open vocabulary. SPEC.md's Event 6 section names three kinds and says
50/// others may be added without a format version bump, so a spelling this crate
51/// does not know is kept verbatim in [`Other`](ErrorKind::Other) rather than
52/// refused. That is the treatment [`Perspective`](crate::header::Perspective)
53/// gets, and it is the rule SPEC.md points at for this key.
54///
55/// An enum and not a bare `String` because these three are the recorder's own
56/// classification of what it just saw, chosen at the point the event is built:
57/// a misspelling is a value no reader can group by, and the compiler is the
58/// only thing that catches it before the trace is written. The wire spellings
59/// live in [`as_str`](ErrorKind::as_str) alone, so a reader and a writer cannot
60/// disagree about them.
61#[derive(Debug, Clone, PartialEq, Eq)]
62#[non_exhaustive]
63pub enum ErrorKind {
64 /// The peer violated the protocol.
65 Protocol,
66 /// The QUIC or WebTransport layer failed.
67 Transport,
68 /// Bytes that would not parse as any message this recorder knows.
69 Decode,
70 /// A kind this version of the crate does not know, kept verbatim.
71 Other(String),
72}
73
74impl ErrorKind {
75 /// The wire spelling of this kind.
76 pub fn as_str(&self) -> &str {
77 match self {
78 ErrorKind::Protocol => "protocol",
79 ErrorKind::Transport => "transport",
80 ErrorKind::Decode => "decode",
81 ErrorKind::Other(s) => s,
82 }
83 }
84
85 fn parse(s: &str) -> Self {
86 match s {
87 "protocol" => ErrorKind::Protocol,
88 "transport" => ErrorKind::Transport,
89 "decode" => ErrorKind::Decode,
90 other => ErrorKind::Other(other.to_string()),
91 }
92 }
93}
94
95/// Which end of a relay a peer sits on.
96///
97/// This is the direction the *connection* was made in, not the direction
98/// subscriptions flow. A downstream peer may publish and an upstream peer may
99/// subscribe; for subscription causality see
100/// [`EventData::SubscriptionDerivation`].
101#[derive(Debug, Clone, PartialEq, Eq, Hash)]
102#[non_exhaustive]
103pub enum Side {
104 /// The peer connected to this relay.
105 Downstream,
106 /// This relay connected to the peer — an origin or an upstream relay.
107 Upstream,
108 /// A side this version of the crate does not know, kept verbatim.
109 Other(String),
110}
111
112impl Side {
113 /// The wire spelling of this side.
114 pub fn as_str(&self) -> &str {
115 match self {
116 Side::Downstream => "downstream",
117 Side::Upstream => "upstream",
118 Side::Other(s) => s,
119 }
120 }
121
122 fn parse(s: &str) -> Self {
123 match s {
124 "downstream" => Side::Downstream,
125 "upstream" => Side::Upstream,
126 other => Side::Other(other.to_string()),
127 }
128 }
129}
130
131/// Role a peer is acting in at connection time.
132#[derive(Debug, Clone, PartialEq, Eq)]
133#[non_exhaustive]
134pub enum PeerRole {
135 /// Peer is acting as a publisher.
136 Publisher,
137 /// Peer is acting as a subscriber.
138 Subscriber,
139 /// Peer is acting as both.
140 Both,
141 /// A role this version of the crate does not know, kept verbatim.
142 Other(String),
143}
144
145impl PeerRole {
146 /// The wire spelling of this role.
147 pub fn as_str(&self) -> &str {
148 match self {
149 PeerRole::Publisher => "publisher",
150 PeerRole::Subscriber => "subscriber",
151 PeerRole::Both => "both",
152 PeerRole::Other(s) => s,
153 }
154 }
155
156 fn parse(s: &str) -> Self {
157 match s {
158 "publisher" => PeerRole::Publisher,
159 "subscriber" => PeerRole::Subscriber,
160 "both" => PeerRole::Both,
161 other => PeerRole::Other(other.to_string()),
162 }
163 }
164}
165
166/// How an upstream subscription came to serve a downstream one.
167#[derive(Debug, Clone, PartialEq, Eq)]
168#[non_exhaustive]
169pub enum DerivationKind {
170 /// A new upstream subscription was created to satisfy downstream subs.
171 Created,
172 /// An existing upstream subscription now also serves an additional
173 /// downstream sub — subscription fan-in.
174 Shared,
175 /// A kind this version of the crate does not know, kept verbatim.
176 Other(String),
177}
178
179impl DerivationKind {
180 /// The wire spelling of this kind.
181 pub fn as_str(&self) -> &str {
182 match self {
183 DerivationKind::Created => "created",
184 DerivationKind::Shared => "shared",
185 DerivationKind::Other(s) => s,
186 }
187 }
188
189 fn parse(s: &str) -> Self {
190 match s {
191 "created" => DerivationKind::Created,
192 "shared" => DerivationKind::Shared,
193 other => DerivationKind::Other(other.to_string()),
194 }
195 }
196}
197
198/// A `(peer, request id)` reference naming one subscription on one peer.
199///
200/// Request IDs are only unique within a peer's session, so neither half
201/// identifies a subscription on its own.
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct SubscriptionRef {
204 /// Source-local peer identifier.
205 pub peer: String,
206 /// Request ID of the subscription on that peer.
207 pub request_id: u64,
208}
209
210impl SubscriptionRef {
211 /// A reference to `request_id` on `peer`.
212 pub fn new(peer: impl Into<String>, request_id: u64) -> Self {
213 SubscriptionRef { peer: peer.into(), request_id }
214 }
215
216 fn to_value(&self) -> Value {
217 Value::Array(vec![Value::Text(self.peer.clone()), Value::Integer(self.request_id.into())])
218 }
219
220 fn from_value(v: &Value) -> Result<Self, MoqTraceError> {
221 let arr = match v {
222 Value::Array(a) => a,
223 _ => {
224 return Err(MoqTraceError::InvalidEvent(
225 "subscription ref is not a CBOR array".into(),
226 ))
227 }
228 };
229 if arr.len() != 2 {
230 return Err(MoqTraceError::InvalidEvent(
231 "subscription ref must be [peer, request_id]".into(),
232 ));
233 }
234 let peer = arr[0]
235 .as_text()
236 .ok_or_else(|| MoqTraceError::InvalidEvent("subscription ref peer is not text".into()))?
237 .to_string();
238 let request_id = as_u64(&arr[1]).ok_or_else(|| {
239 MoqTraceError::InvalidEvent("subscription ref request_id is not uint".into())
240 })?;
241 Ok(SubscriptionRef { peer, request_id })
242 }
243}
244
245/// Length of a trace ID, in bytes. Fixed by the format.
246pub const TRACE_ID_LEN: usize = 16;
247
248/// The most bytes a recorder may put in an [`Error`](EventData::Error) event's
249/// `"raw"`. Fixed by the format — SPEC.md, Event 6, under the heading about
250/// the cap and `"rawlen"`.
251///
252/// The cap binds the party building an event out of bytes it has just
253/// observed, and nothing else. It is deliberately **not** enforced when an
254/// event is serialized, and this crate does not enforce it there: a serializer
255/// cannot tell a freshly recorded event from one that arrived by being read,
256/// so a cap applied at that point either shortens evidence on a rewrite or
257/// refuses a file the reader was required to accept — whichever it does, it
258/// does to the wrong events. A `"raw"` longer than this reads back at its full
259/// length and is written back at its full length.
260///
261/// [`EventData::error_observed`] is where the cap belongs and where this crate
262/// applies it. A recorder assembling the variant by hand applies it here.
263///
264/// One obligation stays with the recorder and cannot live in a constructor at
265/// all: SPEC.md allows `"raw"` once per flow — per stream where the error names
266/// one, per peer where it does not. Later errors on that flow are still
267/// recorded and simply carry no bytes. That is state across events, so it
268/// belongs to whatever is holding the flow.
269pub const ERROR_RAW_CAP: usize = 4096;
270
271/// Event type discriminants, matching the specification's `"e"` values.
272const EVENT_CONTROL_MESSAGE: u64 = 0;
273const EVENT_STREAM_OPENED: u64 = 1;
274const EVENT_STREAM_CLOSED: u64 = 2;
275const EVENT_OBJECT_HEADER: u64 = 3;
276const EVENT_OBJECT_PAYLOAD: u64 = 4;
277const EVENT_STATE_CHANGE: u64 = 5;
278const EVENT_ERROR: u64 = 6;
279const EVENT_ANNOTATION: u64 = 7;
280const EVENT_PEER_CONNECTED: u64 = 8;
281const EVENT_PEER_DISCONNECTED: u64 = 9;
282const EVENT_SUBSCRIPTION_DERIVATION: u64 = 10;
283
284/// A single event in a `.moqtrace` file.
285#[derive(Debug, Clone, PartialEq)]
286pub struct TraceEvent {
287 /// Monotonically increasing sequence number (0-based). Segment-local in a
288 /// segmented trace, so ordering across segments is `(segment, seq)`.
289 pub seq: u64,
290 /// Timestamp in microseconds since the containing segment's start time.
291 pub timestamp: i64,
292 /// Which peer this event pertains to.
293 ///
294 /// Required when the trace's perspective is
295 /// [`RelayTap`](crate::header::Perspective::RelayTap), where a single
296 /// trace covers many concurrent sessions; omitted otherwise, since a
297 /// single-session trace has only one peer to speak of. The identifier is
298 /// **source-local**: the same string in two traces from different sources
299 /// does not name the same peer.
300 pub peer: Option<String>,
301 /// Event-specific data.
302 pub data: EventData,
303 /// Keys on this event that this version of the crate could not use, kept
304 /// verbatim.
305 ///
306 /// Optional keys may be added to an existing event type without a format
307 /// version bump, so "unknown keys MUST be ignored" is a rule about
308 /// *reading past* them. It is not a licence to drop them: a tool that
309 /// reads a trace and writes it back — a redaction pass, a filter, a
310 /// re-segmentation — would otherwise emit a valid file that looks like it
311 /// never carried them, and one tool's ignorance would become permanent for
312 /// every reader downstream of it.
313 ///
314 /// A key this crate *does* know lands here too when its value is not of a
315 /// type that key can hold — `"ta": "hello"` on an event 1, say. SPEC.md
316 /// treats such a key as unrecognised: the value is ignored for meaning,
317 /// the field that would have held it reads `None`, and the entry is
318 /// written back unchanged. Knowing more about a key must not mean
319 /// preserving it less.
320 ///
321 /// [`EventData::Unknown`] already does this for an event type the crate
322 /// cannot name. This is the same guarantee one level down, for a key on a
323 /// type it can.
324 ///
325 /// Unchanged binds the value and not its encoding, exactly as it does for
326 /// [`TraceHeader::extra`](crate::header::TraceHeader::extra): on the way
327 /// out an integral float in a stored value is written as a CBOR integer
328 /// and a byte string under RFC 8746's tag 64 as major type 2, at any
329 /// depth. SPEC.md's two encoding rules are about every byte a writer
330 /// emits rather than only the keys it understood, and the JavaScript
331 /// implementation's decoder folds both shapes away before its own code
332 /// runs, so it could not emit either however hard it tried. Nothing a
333 /// comparison of the two values can see changes, with the single
334 /// exception SPEC.md names: `-0.0` written as `0` loses its sign.
335 ///
336 /// A CBOR map may not carry one key twice, and this list can: it is an
337 /// ordered list of pairs, not a map. So on the way out an entry naming a
338 /// key the event writes from a field is dropped, and of two entries
339 /// sharing a key the first is written — as it is for a map nested inside
340 /// a stored value, which is a map this crate emits too.
341 ///
342 /// The same two passes govern the other opaque values an event carries:
343 /// a control message's `"msg"`, an annotation's `"data"` and an
344 /// [`EventData::Unknown`]'s fields.
345 ///
346 /// Empty for every event this crate constructs itself.
347 pub extra: Vec<(Value, Value)>,
348}
349
350/// Event-specific payload, discriminated by type.
351#[derive(Debug, Clone, PartialEq)]
352#[non_exhaustive]
353pub enum EventData {
354 /// A control-stream message was sent or received (event type 0).
355 ControlMessage {
356 /// Send or receive.
357 direction: Direction,
358 /// Wire message type ID (e.g. `0x03` for SUBSCRIBE).
359 message_type: u64,
360 /// Decoded message fields, and **not necessarily a map**: match on it
361 /// rather than assuming, or use [`TraceEvent::request_id`].
362 ///
363 /// A *recorder* MUST write a CBOR map keyed in snake_case here, and an
364 /// empty map when it decoded nothing. A *reader* has to take what
365 /// earlier writers produced, and every `capture-*` case in the
366 /// conformance corpus holds a text rendering of the message instead.
367 /// Such a value is handed back verbatim — unaddressable by key, but
368 /// not a reason to reject an event the format forbids dropping — and
369 /// is written back unchanged, because replacing it would destroy the
370 /// only record of a message nobody will see again.
371 ///
372 /// Unchanged binds what the value says and not how it is encoded.
373 /// This is a value the reader never looked at, so the two encoding
374 /// rules reach it on the way out like any other:
375 /// [`TraceEvent::extra`] has the whole of it.
376 message: Value,
377 /// QUIC stream the message travelled on, when the recorder knows it.
378 ///
379 /// `None` means unknown, which a reader must not confuse with stream
380 /// 0. Recorders should fill it in: from draft-17 each request has its
381 /// own bidirectional stream and responses carry no request ID, so the
382 /// stream is the only thing pairing a response with its request.
383 stream_id: Option<u64>,
384 /// Raw wire bytes (only at `full` detail level).
385 raw: Option<Vec<u8>>,
386 },
387 /// A QUIC stream was opened (event type 1).
388 ///
389 /// The four optional identifiers are what a `headers` recording has
390 /// instead of the stream header bytes. No detail level records the bytes
391 /// of a `SUBGROUP_HEADER`, a fetch header or a datagram header, so a value
392 /// carried only there has nothing left to be re-parsed from — and before
393 /// these fields existed the model could express group, object, priority
394 /// and status and nothing else, which left a `headers` trace unable to say
395 /// which track a stream belonged to.
396 ///
397 /// Each is `None` when the recorder did not know it — and also when the
398 /// file carried the key with a value that is not an unsigned integer, in
399 /// which case the entry is kept verbatim in [`TraceEvent::extra`] instead
400 /// of being read here. A reader must not refuse a stream for lacking one:
401 /// every recording predating the keys lacks all four.
402 StreamOpened {
403 /// QUIC stream ID.
404 stream_id: u64,
405 /// Outgoing or incoming.
406 direction: Direction,
407 /// Stream type.
408 stream_type: StreamType,
409 /// Track alias the stream carries. Meaningful on any stream type.
410 track_alias: Option<u64>,
411 /// Subgroup ID, on a [`StreamType::Subgroup`] stream.
412 subgroup_id: Option<u64>,
413 /// Fetch request ID, on a [`StreamType::Fetch`] stream.
414 ///
415 /// A recorder must write it there. It is the only correlation between
416 /// the stream and the FETCH that asked for it, and unlike a subgroup
417 /// stream a fetch stream carries no track alias to identify it by
418 /// instead — so without it a fetch stream in a `headers` trace names
419 /// nothing at all.
420 fetch_request_id: Option<u64>,
421 /// Group ID, on a [`StreamType::Datagram`] stream.
422 ///
423 /// Scoped to datagrams because on a subgroup stream every object
424 /// carries the group of the stream by construction, so a copy here
425 /// would be a second field with no independent source. Where it does
426 /// appear alongside an [`ObjectHeader`](EventData::ObjectHeader) for
427 /// the same stream, the object header is authoritative: this copy
428 /// serves a reader that has not yet seen an object, and a
429 /// disagreement between the two is not corruption.
430 group_id: Option<u64>,
431 },
432 /// A QUIC stream was closed (event type 2).
433 StreamClosed {
434 /// QUIC stream ID.
435 stream_id: u64,
436 /// Error code (0 = clean close).
437 error_code: u64,
438 },
439 /// An object header was parsed from a data stream (event type 3).
440 ObjectHeader {
441 /// Stream ID this object arrived on.
442 stream_id: u64,
443 /// Group ID.
444 group: u64,
445 /// Object ID.
446 object: u64,
447 /// Publisher priority.
448 publisher_priority: u64,
449 /// Object status (0=normal, 1=end-of-group, etc.).
450 object_status: u64,
451 },
452 /// Object payload bytes were received or sent (event type 4).
453 ObjectPayload {
454 /// Stream ID.
455 stream_id: u64,
456 /// Group ID.
457 group: u64,
458 /// Object ID.
459 object: u64,
460 /// Payload size in bytes.
461 size: u64,
462 /// Payload bytes (only at `headers+data` or `full` level).
463 payload: Option<Vec<u8>>,
464 },
465 /// Session FSM phase transition (event type 5).
466 StateChange {
467 /// Previous session phase.
468 from: String,
469 /// New session phase.
470 to: String,
471 },
472 /// Protocol or transport error (event type 6).
473 ///
474 /// The four optional fields are what makes the event evidence rather than
475 /// an assertion. A peer that sends something malformed is one of the few
476 /// things a shared trace is uniquely good for — the recording party can
477 /// see it and the sending party cannot — and until these existed the only
478 /// field in the format able to hold bytes was a control message's `"raw"`,
479 /// so a recorder wanting to keep the offending bytes had to record the
480 /// violation as a decodable message in order to have somewhere to put
481 /// them.
482 ///
483 /// Each is `None` when the recorder did not write it — and also when the
484 /// file carried the key with a value the field cannot hold, in which case
485 /// the entry stays verbatim in [`TraceEvent::extra`] instead of being read
486 /// here.
487 ///
488 /// [`error_observed`](EventData::error_observed) builds one of these from
489 /// bytes a recorder has just seen, which is where the cap on `raw` and the
490 /// detail levels the two byte-bearing fields sit at are applied.
491 Error {
492 /// Error code.
493 error_code: u64,
494 /// Human-readable reason.
495 reason: String,
496 /// QUIC stream the error was observed on, when there was one and the
497 /// recorder knows it.
498 ///
499 /// Optional on the same terms as a control message's `"sid"`: `None`
500 /// means there was no stream or none is known, which a reader must not
501 /// confuse with stream 0.
502 stream_id: Option<u64>,
503 /// What sort of failure this was.
504 kind: Option<ErrorKind>,
505 /// Byte length of the input the recorder held for this error, before
506 /// any truncation.
507 ///
508 /// Recorded from `headers+sizes` upwards, one level below the bytes
509 /// themselves: how large a malformed message was is often enough on
510 /// its own to tell a truncated message from a mistyped one, and it
511 /// carries no content. It is still a size, and this format gates sizes
512 /// deliberately, so it does not reach a `control`-level trace.
513 ///
514 /// Where both are present, a value larger than `raw`'s length is how a
515 /// reader learns the capture is partial and by how much. Where this is
516 /// absent, a `raw` of exactly [`ERROR_RAW_CAP`] bytes is the one length
517 /// the cap makes ambiguous and must be taken as possibly truncated.
518 raw_len: Option<u64>,
519 /// The offending bytes, at `full` detail only.
520 ///
521 /// Payload-bearing, and gated a level above the event's own
522 /// `control`+: an error naming a *data* stream has subgroup framing
523 /// and object payload behind it, so inheriting the event's level would
524 /// have put media into traces whose declared level excludes payloads
525 /// outright.
526 ///
527 /// A recorder caps this at [`ERROR_RAW_CAP`] bytes. A reader does not:
528 /// a longer one read from a file is neither shortened nor refused.
529 raw: Option<Vec<u8>>,
530 },
531 /// User-defined annotation (event type 7).
532 Annotation {
533 /// User-defined label.
534 label: String,
535 /// User-defined data (any CBOR type).
536 ///
537 /// Opaque: nothing here reads it, and it is written back saying what
538 /// it said. Its encoding is the writer's, though — see
539 /// [`TraceEvent::extra`].
540 data: Value,
541 },
542 /// A new peer session was established (event type 8).
543 ///
544 /// The identifier for the peer lives on [`TraceEvent::peer`]; every later
545 /// event for that peer repeats it.
546 PeerConnected {
547 /// Peer-reported endpoint URI or remote address, best-effort.
548 endpoint: Option<String>,
549 /// Transport type (`"webtransport"`, `"raw-quic"`, …).
550 transport: Option<String>,
551 /// Role at connection time, if known then.
552 role: Option<PeerRole>,
553 /// Which end of the relay the peer connected from.
554 side: Option<Side>,
555 },
556 /// A peer session ended (event type 9).
557 PeerDisconnected {
558 /// Error or close code (0 = clean close).
559 error_code: u64,
560 /// Human-readable reason.
561 reason: Option<String>,
562 },
563 /// An upstream subscription was created or extended in causal response to
564 /// downstream ones (event type 10).
565 ///
566 /// This is the primitive multi-hop correlation is built from: a collector
567 /// reconstructs an end-to-end tree from these links plus the trace IDs
568 /// propagated along them.
569 ///
570 /// The four timestamps share the `timestamp` field's timebase — the
571 /// emitting source's own clock. Differences between them are therefore
572 /// meaningful with no cross-hop clock agreement, which is the point: they
573 /// measure how long *this* hop took. Never subtract one source's
574 /// timestamp from another's.
575 ///
576 /// A source emits the event as soon as the downstream SUBSCRIBE arrives,
577 /// carrying whichever timestamps it has, and may emit it again for the
578 /// same pair as the rest arrive. A consumer treats the later event as an
579 /// update to the earlier one, not as a second derivation.
580 SubscriptionDerivation {
581 /// The upstream subscription.
582 upstream: SubscriptionRef,
583 /// Every downstream subscription currently served by `upstream`.
584 downstream: Vec<SubscriptionRef>,
585 /// Whether the upstream sub was created here or was already running.
586 kind: DerivationKind,
587 /// Trace ID propagated along the subscription chain, carried as raw
588 /// bytes at every layer so two implementations produce identical
589 /// values for the same chain.
590 trace_id: Option<[u8; TRACE_ID_LEN]>,
591 /// Track namespace the subscription targets, one entry per field.
592 namespace: Option<Vec<Vec<u8>>>,
593 /// Track name the subscription targets.
594 track_name: Option<Vec<u8>>,
595 /// When the downstream SUBSCRIBE was received.
596 t_downstream_received: Option<i64>,
597 /// When the upstream SUBSCRIBE was transmitted. Absent for terminal
598 /// subscriptions, where this source is the content origin.
599 t_upstream_sent: Option<i64>,
600 /// When SUBSCRIBE_OK was received from upstream. Absent for terminal
601 /// or still-in-flight subscriptions.
602 t_upstream_ok_received: Option<i64>,
603 /// When SUBSCRIBE_OK was transmitted downstream. Absent while the
604 /// subscription is still in flight.
605 t_downstream_ok_sent: Option<i64>,
606 },
607 /// An event whose type this version of the crate does not know.
608 ///
609 /// New event types may be added without a format version bump, so a
610 /// reader that rejected them would turn every future addition into a
611 /// breaking change. The fields are kept verbatim, which means an unknown
612 /// event survives a read-modify-write round trip intact — intact in what
613 /// it says, since the encoding written back is this crate's own. See
614 /// [`TraceEvent::extra`].
615 Unknown {
616 /// The event type discriminant that was read.
617 event_type: u64,
618 /// Every key and value from the event map other than `"n"`, `"t"`
619 /// and `"e"` — and other than `"p"`, when a peer was read from it. A
620 /// `"p"` that is not text is kept here like any other value this crate
621 /// could not use.
622 fields: Vec<(Value, Value)>,
623 },
624}
625
626/// Where `detail` sits in the levels' ordering, or `None` for a level this
627/// crate cannot place.
628///
629/// The levels are a chain — each records everything the one before it does —
630/// so one rank answers every question about which of two a recorder is at.
631/// A level this crate has never heard of has no place in the chain, which is
632/// why this is an `Option` rather than a number with a default: a default
633/// would guess, and the two ways of guessing wrong are not symmetric.
634fn detail_rank(detail: &DetailLevel) -> Option<u8> {
635 match detail {
636 DetailLevel::Control => Some(0),
637 DetailLevel::Headers => Some(1),
638 DetailLevel::HeadersSizes => Some(2),
639 DetailLevel::HeadersData => Some(3),
640 DetailLevel::Full => Some(4),
641 DetailLevel::Other(_) => None,
642 }
643}
644
645/// Whether a recorder at `detail` records everything a recorder at `floor`
646/// does.
647///
648/// `false` when either level is one this crate cannot place. A future level
649/// might sit above `floor` or below it, and answering `true` on a guess would
650/// put payload bytes into a trace whose declared level excludes them — a loss
651/// of one diagnostic against a leak that cannot be taken back.
652fn detail_reaches(detail: &DetailLevel, floor: &DetailLevel) -> bool {
653 match (detail_rank(detail), detail_rank(floor)) {
654 (Some(level), Some(floor)) => level >= floor,
655 _ => false,
656 }
657}
658
659impl EventData {
660 /// An [`Error`](EventData::Error) built from the bytes a recorder has just
661 /// observed, for a trace recorded at `detail`.
662 ///
663 /// This is where the cap on `"raw"` belongs and the only place this crate
664 /// applies it: the party constructing an event out of traffic it just saw
665 /// is the one SPEC.md addresses, and it is the only party that can tell a
666 /// fresh event from one that arrived by being read. Serializing does not
667 /// cap, and reading does not refuse — see [`ERROR_RAW_CAP`].
668 ///
669 /// `observed` is the whole input the recorder held, uncapped. What comes
670 /// back depends on `detail`, because the two byte-bearing fields sit at
671 /// different levels and that is deliberate:
672 ///
673 /// - `raw_len` is the full length of `observed`, from `headers+sizes`
674 /// upwards. It is a size, and sizes are gated; it is not gated *with*
675 /// the bytes, so it is available in every trace where the bytes must not
676 /// appear, which is the point of having it.
677 /// - `raw` is the first [`ERROR_RAW_CAP`] bytes of `observed`, at `full`
678 /// only. Below that level nothing is copied.
679 ///
680 /// A level this crate cannot place yields neither.
681 ///
682 /// Where both come back, comparing them is how a reader learns the capture
683 /// was truncated: `raw_len` is the length before the cap bit, not after.
684 pub fn error_observed(
685 error_code: u64,
686 reason: impl Into<String>,
687 kind: Option<ErrorKind>,
688 stream_id: Option<u64>,
689 observed: &[u8],
690 detail: &DetailLevel,
691 ) -> Self {
692 let raw_len = detail_reaches(detail, &DetailLevel::HeadersSizes)
693 .then(|| u64::try_from(observed.len()).unwrap_or(u64::MAX));
694 let raw = detail_reaches(detail, &DetailLevel::Full)
695 .then(|| observed[..observed.len().min(ERROR_RAW_CAP)].to_vec());
696 EventData::Error { error_code, reason: reason.into(), stream_id, kind, raw_len, raw }
697 }
698}
699
700impl TraceEvent {
701 /// An event with no peer identifier — the single-session case.
702 pub fn new(seq: u64, timestamp: i64, data: EventData) -> Self {
703 TraceEvent { seq, timestamp, peer: None, data, extra: Vec::new() }
704 }
705
706 /// An event attributed to `peer` — the relay-tap case.
707 pub fn for_peer(seq: u64, timestamp: i64, peer: impl Into<String>, data: EventData) -> Self {
708 TraceEvent { seq, timestamp, peer: Some(peer.into()), data, extra: Vec::new() }
709 }
710
711 /// Attach unrecognised keys, for a caller reconstructing an event it did
712 /// not decode itself.
713 ///
714 /// Keys that collide with ones the event writes from its own fields are
715 /// dropped on serialization rather than written twice, since a CBOR map
716 /// with a repeated key is malformed and the event's own value is the one
717 /// the reader would have produced. A key the event's type merely *defines*
718 /// does not collide: an optional field holding `None` writes nothing, so
719 /// the entry here is the only copy of that key and is written.
720 #[must_use]
721 pub fn with_extra(mut self, extra: Vec<(Value, Value)>) -> Self {
722 self.extra = extra;
723 self
724 }
725
726 /// The event type discriminant this event serializes as.
727 pub fn event_type(&self) -> u64 {
728 match &self.data {
729 EventData::ControlMessage { .. } => EVENT_CONTROL_MESSAGE,
730 EventData::StreamOpened { .. } => EVENT_STREAM_OPENED,
731 EventData::StreamClosed { .. } => EVENT_STREAM_CLOSED,
732 EventData::ObjectHeader { .. } => EVENT_OBJECT_HEADER,
733 EventData::ObjectPayload { .. } => EVENT_OBJECT_PAYLOAD,
734 EventData::StateChange { .. } => EVENT_STATE_CHANGE,
735 EventData::Error { .. } => EVENT_ERROR,
736 EventData::Annotation { .. } => EVENT_ANNOTATION,
737 EventData::PeerConnected { .. } => EVENT_PEER_CONNECTED,
738 EventData::PeerDisconnected { .. } => EVENT_PEER_DISCONNECTED,
739 EventData::SubscriptionDerivation { .. } => EVENT_SUBSCRIPTION_DERIVATION,
740 EventData::Unknown { event_type, .. } => *event_type,
741 }
742 }
743
744 /// Extract the `request_id` from a control message's decoded `"msg"`
745 /// field, if present.
746 ///
747 /// Returns `None` for non-control-message events, for a `"msg"` that is
748 /// not a map, and for a map that names the field something else. Drafts 07
749 /// through 10 call it `subscribe_id`, and this does not answer for them:
750 /// the two names sit on different messages with different meanings, and a
751 /// reader that wants either can ask the map itself.
752 ///
753 /// The key is `request_id`, in the snake_case the drafts use, because that
754 /// is what every writer of these files produces. It read `requestId` until
755 /// draft-20, and matched nothing — not this crate's own corpus, and not a
756 /// trace written by any other implementation.
757 pub fn request_id(&self) -> Option<u64> {
758 if let EventData::ControlMessage { message: Value::Map(ref pairs), .. } = self.data {
759 for (k, v) in pairs {
760 if k.as_text() == Some("request_id") {
761 return v.as_integer().and_then(|i| u64::try_from(i).ok());
762 }
763 }
764 }
765 None
766 }
767
768 /// Return the message type for control message events.
769 pub fn message_type(&self) -> Option<u64> {
770 if let EventData::ControlMessage { message_type, .. } = self.data {
771 Some(message_type)
772 } else {
773 None
774 }
775 }
776
777 /// Return the direction for events that have one.
778 pub fn direction(&self) -> Option<Direction> {
779 match &self.data {
780 EventData::ControlMessage { direction, .. }
781 | EventData::StreamOpened { direction, .. } => Some(*direction),
782 _ => None,
783 }
784 }
785}
786
787// ── CBOR conversion ────────────────────────────────────────
788
789/// Wrapper that serializes as a CBOR byte string (major type 2). Without this
790/// wrapper, serializing `&[u8]` via generic `Serialize` produces a CBOR array
791/// of u8.
792struct ByteStr<'a>(&'a [u8]);
793
794impl serde::Serialize for ByteStr<'_> {
795 #[inline]
796 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
797 s.serialize_bytes(self.0)
798 }
799}
800
801impl Direction {
802 #[inline]
803 fn to_u64(self) -> u64 {
804 match self {
805 Direction::Send => 0,
806 Direction::Receive => 1,
807 }
808 }
809}
810
811impl StreamType {
812 #[inline]
813 fn to_u64(self) -> u64 {
814 match self {
815 StreamType::Subgroup => 0,
816 StreamType::Datagram => 1,
817 StreamType::Fetch => 2,
818 }
819 }
820}
821
822/// Build a CBOR [`Value`] from a [`TraceEvent`] by running it through the
823/// crate's `Serialize` impl. Convenience for tests and inspection — the
824/// hot write path in [`MoqTraceWriter`](crate::writer::MoqTraceWriter) uses
825/// `Serialize` directly and never materializes a `Value`.
826impl From<&TraceEvent> for Value {
827 fn from(event: &TraceEvent) -> Self {
828 Value::serialized(event).expect("TraceEvent serialization is infallible")
829 }
830}
831
832impl serde::Serialize for TraceEvent {
833 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
834 use serde::ser::SerializeMap;
835
836 // Every value an event carries out of a file rather than out of a
837 // typed field goes out through `normalised`: an unknown event type's
838 // fields and the store here, a control message's `"msg"` and an
839 // annotation's `"data"` below. The two encoding rules bind the bytes a
840 // writer emits and so bind every value it emits, the ones it never
841 // looked at included — SPEC.md, Interoperability. A typed field needs
842 // none of this and gets none: `"raw"`, `"pl"`, `"traceId"`, `"tn"` and
843 // every field of `"ns"` are `Vec<u8>` by the time they reach here and
844 // go out as major type 2 by construction, and every integer field goes
845 // out as a CBOR integer for the same reason.
846 //
847 // Filtered against the common fields on the way out, the same way the
848 // store below is filtered against every field the event writes: an
849 // unknown event type writes `"n"`, `"t"`, `"e"` and `"p"` from the
850 // common fields, so an entry here naming one of those is dropped
851 // rather than putting that key in the map twice. A caller can put one
852 // in this list by hand, and a file that repeated a common key leaves
853 // one here too — that is where the entry `seq` did not take now lives.
854 let unknown_fields = match &self.data {
855 EventData::Unknown { fields, .. } => {
856 store_entries(fields, |key| writes_common_key(self.peer.as_deref(), key))
857 }
858 _ => Vec::new(),
859 };
860
861 // A key this event writes from one of its own fields is written from
862 // there, so an `extra` entry naming it is dropped rather than written
863 // twice: a CBOR map with a duplicate key is malformed, and the field
864 // is what a reader produced. A key the type merely *defines* is not
865 // one of those — an optional field holding `None` writes nothing, and
866 // the entry here is then a value the decode could not use, which has
867 // to go back out.
868 //
869 // The same pass the header's stores go through, for the same three
870 // reasons: the filter above, the encoding rules, and no key twice.
871 let extra = store_entries(&self.extra, |key| {
872 writes_common_key(self.peer.as_deref(), key) || writes_variant_key(&self.data, key)
873 });
874
875 // Pre-count entries so the CBOR map is encoded with a definite length.
876 let variant_entries = match &self.data {
877 EventData::ControlMessage { stream_id, raw, .. } => {
878 3 + usize::from(stream_id.is_some()) + usize::from(raw.is_some())
879 }
880 EventData::StreamOpened {
881 track_alias,
882 subgroup_id,
883 fetch_request_id,
884 group_id,
885 ..
886 } => {
887 3 + usize::from(track_alias.is_some())
888 + usize::from(subgroup_id.is_some())
889 + usize::from(fetch_request_id.is_some())
890 + usize::from(group_id.is_some())
891 }
892 EventData::StreamClosed { .. } => 2,
893 EventData::ObjectHeader { .. } => 5,
894 EventData::ObjectPayload { payload, .. } => 4 + usize::from(payload.is_some()),
895 EventData::StateChange { .. } => 2,
896 EventData::Error { stream_id, kind, raw_len, raw, .. } => {
897 2 + usize::from(stream_id.is_some())
898 + usize::from(kind.is_some())
899 + usize::from(raw_len.is_some())
900 + usize::from(raw.is_some())
901 }
902 EventData::Annotation { .. } => 2,
903 EventData::PeerConnected { endpoint, transport, role, side } => {
904 usize::from(endpoint.is_some())
905 + usize::from(transport.is_some())
906 + usize::from(role.is_some())
907 + usize::from(side.is_some())
908 }
909 EventData::PeerDisconnected { reason, .. } => 1 + usize::from(reason.is_some()),
910 EventData::SubscriptionDerivation {
911 trace_id,
912 namespace,
913 track_name,
914 t_downstream_received,
915 t_upstream_sent,
916 t_upstream_ok_received,
917 t_downstream_ok_sent,
918 ..
919 } => {
920 3 + usize::from(trace_id.is_some())
921 + usize::from(namespace.is_some())
922 + usize::from(track_name.is_some())
923 + usize::from(t_downstream_received.is_some())
924 + usize::from(t_upstream_sent.is_some())
925 + usize::from(t_upstream_ok_received.is_some())
926 + usize::from(t_downstream_ok_sent.is_some())
927 }
928 EventData::Unknown { .. } => unknown_fields.len(),
929 };
930
931 let entries = 3 /* n, t, e */
932 + usize::from(self.peer.is_some())
933 + variant_entries
934 + extra.len();
935
936 let mut map = ser.serialize_map(Some(entries))?;
937 map.serialize_entry("n", &self.seq)?;
938 map.serialize_entry("t", &self.timestamp)?;
939 if let Some(ref peer) = self.peer {
940 map.serialize_entry("p", peer)?;
941 }
942
943 match &self.data {
944 EventData::ControlMessage { direction, message_type, message, stream_id, raw } => {
945 map.serialize_entry("e", &EVENT_CONTROL_MESSAGE)?;
946 map.serialize_entry("d", &direction.to_u64())?;
947 map.serialize_entry("mt", message_type)?;
948 map.serialize_entry("msg", &normalised(message))?;
949 if let Some(sid) = stream_id {
950 map.serialize_entry("sid", sid)?;
951 }
952 if let Some(raw) = raw {
953 map.serialize_entry("raw", &ByteStr(raw))?;
954 }
955 }
956 EventData::StreamOpened {
957 stream_id,
958 direction,
959 stream_type,
960 track_alias,
961 subgroup_id,
962 fetch_request_id,
963 group_id,
964 } => {
965 map.serialize_entry("e", &EVENT_STREAM_OPENED)?;
966 map.serialize_entry("sid", stream_id)?;
967 map.serialize_entry("d", &direction.to_u64())?;
968 map.serialize_entry("st", &stream_type.to_u64())?;
969 if let Some(ta) = track_alias {
970 map.serialize_entry("ta", ta)?;
971 }
972 if let Some(sg) = subgroup_id {
973 map.serialize_entry("sg", sg)?;
974 }
975 if let Some(fri) = fetch_request_id {
976 map.serialize_entry("fri", fri)?;
977 }
978 if let Some(g) = group_id {
979 map.serialize_entry("g", g)?;
980 }
981 }
982 EventData::StreamClosed { stream_id, error_code } => {
983 map.serialize_entry("e", &EVENT_STREAM_CLOSED)?;
984 map.serialize_entry("sid", stream_id)?;
985 map.serialize_entry("ec", error_code)?;
986 }
987 EventData::ObjectHeader {
988 stream_id,
989 group,
990 object,
991 publisher_priority,
992 object_status,
993 } => {
994 map.serialize_entry("e", &EVENT_OBJECT_HEADER)?;
995 map.serialize_entry("sid", stream_id)?;
996 map.serialize_entry("g", group)?;
997 map.serialize_entry("o", object)?;
998 map.serialize_entry("pp", publisher_priority)?;
999 map.serialize_entry("os", object_status)?;
1000 }
1001 EventData::ObjectPayload { stream_id, group, object, size, payload } => {
1002 map.serialize_entry("e", &EVENT_OBJECT_PAYLOAD)?;
1003 map.serialize_entry("sid", stream_id)?;
1004 map.serialize_entry("g", group)?;
1005 map.serialize_entry("o", object)?;
1006 map.serialize_entry("sz", size)?;
1007 if let Some(pl) = payload {
1008 map.serialize_entry("pl", &ByteStr(pl))?;
1009 }
1010 }
1011 EventData::StateChange { from, to } => {
1012 map.serialize_entry("e", &EVENT_STATE_CHANGE)?;
1013 map.serialize_entry("from", from)?;
1014 map.serialize_entry("to", to)?;
1015 }
1016 EventData::Error { error_code, reason, stream_id, kind, raw_len, raw } => {
1017 map.serialize_entry("e", &EVENT_ERROR)?;
1018 map.serialize_entry("ec", error_code)?;
1019 map.serialize_entry("reason", reason)?;
1020 if let Some(sid) = stream_id {
1021 map.serialize_entry("sid", sid)?;
1022 }
1023 if let Some(kind) = kind {
1024 map.serialize_entry("ek", kind.as_str())?;
1025 }
1026 if let Some(raw_len) = raw_len {
1027 map.serialize_entry("rawlen", raw_len)?;
1028 }
1029 // At whatever length it arrived. The cap is the recorder's,
1030 // and re-applying it here would shorten evidence read out of
1031 // somebody else's file — see [`ERROR_RAW_CAP`].
1032 if let Some(raw) = raw {
1033 map.serialize_entry("raw", &ByteStr(raw))?;
1034 }
1035 }
1036 EventData::Annotation { label, data } => {
1037 map.serialize_entry("e", &EVENT_ANNOTATION)?;
1038 map.serialize_entry("label", label)?;
1039 map.serialize_entry("data", &normalised(data))?;
1040 }
1041 EventData::PeerConnected { endpoint, transport, role, side } => {
1042 map.serialize_entry("e", &EVENT_PEER_CONNECTED)?;
1043 if let Some(endpoint) = endpoint {
1044 map.serialize_entry("endpoint", endpoint)?;
1045 }
1046 if let Some(transport) = transport {
1047 map.serialize_entry("transport", transport)?;
1048 }
1049 if let Some(role) = role {
1050 map.serialize_entry("role", role.as_str())?;
1051 }
1052 if let Some(side) = side {
1053 map.serialize_entry("side", side.as_str())?;
1054 }
1055 }
1056 EventData::PeerDisconnected { error_code, reason } => {
1057 map.serialize_entry("e", &EVENT_PEER_DISCONNECTED)?;
1058 map.serialize_entry("ec", error_code)?;
1059 if let Some(reason) = reason {
1060 map.serialize_entry("reason", reason)?;
1061 }
1062 }
1063 EventData::SubscriptionDerivation {
1064 upstream,
1065 downstream,
1066 kind,
1067 trace_id,
1068 namespace,
1069 track_name,
1070 t_downstream_received,
1071 t_upstream_sent,
1072 t_upstream_ok_received,
1073 t_downstream_ok_sent,
1074 } => {
1075 map.serialize_entry("e", &EVENT_SUBSCRIPTION_DERIVATION)?;
1076 map.serialize_entry("u", &upstream.to_value())?;
1077 let downstream_refs: Vec<Value> =
1078 downstream.iter().map(SubscriptionRef::to_value).collect();
1079 map.serialize_entry("d", &Value::Array(downstream_refs))?;
1080 map.serialize_entry("kind", kind.as_str())?;
1081 if let Some(trace_id) = trace_id {
1082 map.serialize_entry("traceId", &ByteStr(trace_id))?;
1083 }
1084 if let Some(namespace) = namespace {
1085 let fields: Vec<Value> =
1086 namespace.iter().map(|f| Value::Bytes(f.clone())).collect();
1087 map.serialize_entry("ns", &Value::Array(fields))?;
1088 }
1089 if let Some(track_name) = track_name {
1090 map.serialize_entry("tn", &ByteStr(track_name))?;
1091 }
1092 if let Some(t) = t_downstream_received {
1093 map.serialize_entry("tdr", t)?;
1094 }
1095 if let Some(t) = t_upstream_sent {
1096 map.serialize_entry("tus", t)?;
1097 }
1098 if let Some(t) = t_upstream_ok_received {
1099 map.serialize_entry("tuo", t)?;
1100 }
1101 if let Some(t) = t_downstream_ok_sent {
1102 map.serialize_entry("tdo", t)?;
1103 }
1104 }
1105 EventData::Unknown { event_type, .. } => {
1106 map.serialize_entry("e", event_type)?;
1107 for (k, v) in &unknown_fields {
1108 map.serialize_entry(k, v)?;
1109 }
1110 }
1111 }
1112
1113 // Last, so the event's own keys keep the positions a reader expects
1114 // and the file stays diffable against one written without them.
1115 for (k, v) in &extra {
1116 map.serialize_entry(k, v)?;
1117 }
1118
1119 map.end()
1120 }
1121}
1122
1123// ── decoding helpers ───────────────────────────────────────
1124
1125/// Helper to extract a u64 from a CBOR map by key.
1126///
1127/// Accepts the float form an encoder may use for a value past 32 bits — see
1128/// [`as_u64`](crate::header::as_u64).
1129fn get_uint(pairs: &[(Value, Value)], key: &str) -> Option<u64> {
1130 pairs.iter().find_map(|(k, v)| if k.as_text() == Some(key) { as_u64(v) } else { None })
1131}
1132
1133/// Helper to extract an i64 from a CBOR map by key. See [`get_uint`].
1134fn get_int(pairs: &[(Value, Value)], key: &str) -> Option<i64> {
1135 pairs.iter().find_map(|(k, v)| if k.as_text() == Some(key) { as_i64(v) } else { None })
1136}
1137
1138/// Helper to extract a text string from a CBOR map by key.
1139fn get_text(pairs: &[(Value, Value)], key: &str) -> Option<String> {
1140 pairs.iter().find_map(|(k, v)| {
1141 if k.as_text() == Some(key) {
1142 v.as_text().map(|s| s.to_string())
1143 } else {
1144 None
1145 }
1146 })
1147}
1148
1149/// Helper to extract a value from a CBOR map by key.
1150fn get_value(pairs: &[(Value, Value)], key: &str) -> Option<Value> {
1151 pairs.iter().find_map(|(k, v)| if k.as_text() == Some(key) { Some(v.clone()) } else { None })
1152}
1153
1154/// Tag 64 marks a byte string as an array of unsigned 8-bit integers.
1155const TAG_UINT8_ARRAY: u64 = 64;
1156
1157/// Read a byte string, unwrapping the typed-array tag some encoders add.
1158///
1159/// A byte string may arrive plain or wrapped in tag 64, and one of the two
1160/// implementations of this format wraps every one it writes. Reading only the
1161/// plain form meant every payload-bearing field that encoder produced — raw
1162/// wire bytes, object payloads, track names, trace ids — read as absent, and
1163/// a payload-bearing field that reads as absent is indistinguishable from one
1164/// the recorder chose not to capture.
1165fn as_bytes(value: &Value) -> Option<Vec<u8>> {
1166 match value {
1167 Value::Bytes(b) => Some(b.clone()),
1168 Value::Tag(TAG_UINT8_ARRAY, inner) => match inner.as_ref() {
1169 Value::Bytes(b) => Some(b.clone()),
1170 _ => None,
1171 },
1172 _ => None,
1173 }
1174}
1175
1176/// Helper to extract byte string from a CBOR map by key.
1177fn get_bytes(pairs: &[(Value, Value)], key: &str) -> Option<Vec<u8>> {
1178 pairs.iter().find_map(|(k, v)| if k.as_text() == Some(key) { as_bytes(v) } else { None })
1179}
1180
1181/// Read a control message's decoded `"msg"` field.
1182///
1183/// An absent key reads as the empty map a writer should have written, not as
1184/// an error. The format requires a writer with nothing decoded — an
1185/// unparseable message type, or a recorder not decoding bodies at all — to
1186/// emit `{}` rather than omit the key, but requires a reader to be more
1187/// tolerant still, and this is why: event 0 is one of the types sampling MUST
1188/// NOT drop, so refusing it over a missing `"msg"` discards exactly the events
1189/// the format promises to keep.
1190///
1191/// How much is lost depends on how the caller drives the reader, and the worse
1192/// case is the idiomatic one. [`MoqTraceReader::read_next`] returns the error,
1193/// so `collect::<Result<Vec<_>, _>>()` — what this crate's own tests use —
1194/// stops at the first offending event and yields none of the ones after it. A
1195/// caller that skips errors and continues loses only the offending events;
1196/// `moqtap trace` does that, and prints each one, so the loss was at least
1197/// visible there.
1198///
1199/// A value that is present is kept whatever its type. Recordings predating
1200/// the rule carry a text rendering of the message here — every `capture-*`
1201/// case in the conformance corpus is such a file — and they stay readable,
1202/// with the field simply not addressable by key.
1203fn get_message(pairs: &[(Value, Value)]) -> Value {
1204 get_value(pairs, "msg").unwrap_or_else(|| Value::Map(Vec::new()))
1205}
1206
1207fn require_uint(pairs: &[(Value, Value)], key: &str) -> Result<u64, MoqTraceError> {
1208 get_uint(pairs, key).ok_or_else(|| MoqTraceError::InvalidEvent(format!("missing '{key}'")))
1209}
1210
1211fn require_int(pairs: &[(Value, Value)], key: &str) -> Result<i64, MoqTraceError> {
1212 get_int(pairs, key).ok_or_else(|| MoqTraceError::InvalidEvent(format!("missing '{key}'")))
1213}
1214
1215fn require_text(pairs: &[(Value, Value)], key: &str) -> Result<String, MoqTraceError> {
1216 get_text(pairs, key).ok_or_else(|| MoqTraceError::InvalidEvent(format!("missing '{key}'")))
1217}
1218
1219fn require_value(pairs: &[(Value, Value)], key: &str) -> Result<Value, MoqTraceError> {
1220 get_value(pairs, key).ok_or_else(|| MoqTraceError::InvalidEvent(format!("missing '{key}'")))
1221}
1222
1223fn require_direction(pairs: &[(Value, Value)], key: &str) -> Result<Direction, MoqTraceError> {
1224 let v = require_value(pairs, key)?;
1225 Direction::from_cbor(&v)
1226}
1227
1228/// Read a trace ID, refusing any length but the one the format fixes.
1229///
1230/// A wrong-length value is malformed rather than something to pad or
1231/// truncate: the identifier exists so that two implementations independently
1232/// derive byte-identical values for one subscription chain, and a reader that
1233/// quietly reshapes it breaks exactly the property it is there for.
1234fn get_trace_id(pairs: &[(Value, Value)]) -> Result<Option<[u8; TRACE_ID_LEN]>, MoqTraceError> {
1235 let Some(bytes) = get_bytes(pairs, "traceId") else {
1236 return Ok(None);
1237 };
1238 <[u8; TRACE_ID_LEN]>::try_from(bytes.as_slice()).map(Some).map_err(|_| {
1239 MoqTraceError::InvalidEvent(format!(
1240 "'traceId' is {} bytes, must be {TRACE_ID_LEN}",
1241 bytes.len()
1242 ))
1243 })
1244}
1245
1246/// Read a track namespace: an array of byte strings, tolerating text entries
1247/// from encoders that lose the distinction.
1248///
1249/// Answers `None` for anything else — a `"ns"` that is not an array, or an
1250/// array holding something that is neither bytes nor text. `"ns"` is optional,
1251/// and the rule for an optional key whose value this reader cannot use is that
1252/// the key is kept as an unrecognised one rather than that the event dies; the
1253/// caller then still has the derivation's upstream and downstream
1254/// subscriptions, which are what make it a derivation at all.
1255///
1256/// It used to return an error, which cost more than the field. `read_next`
1257/// propagates it, so `collect::<Result<Vec<_>, _>>()` — the documented idiom —
1258/// stopped at that event and yielded none of the ones after it. One namespace
1259/// an encoder wrote oddly took the rest of the recording with it.
1260fn get_namespace(pairs: &[(Value, Value)]) -> Option<Vec<Vec<u8>>> {
1261 let Some(Value::Array(items)) = get_value(pairs, "ns") else {
1262 return None;
1263 };
1264 let mut namespace = Vec::with_capacity(items.len());
1265 for item in items {
1266 let field = as_bytes(&item).or_else(|| item.as_text().map(|t| t.as_bytes().to_vec()))?;
1267 namespace.push(field);
1268 }
1269 Some(namespace)
1270}
1271
1272/// The three keys every event carries whatever its type. All are required, so
1273/// a value the reader cannot use is a malformed event rather than something to
1274/// keep: there would be no event left to hang the kept key on.
1275const REQUIRED_COMMON_KEYS: [&str; 3] = ["n", "t", "e"];
1276
1277/// Whether the common event fields account for `key` — the required three
1278/// always, and `"p"` only when a peer was actually read from it.
1279///
1280/// `"p"` is optional, and optional means it can also be *unusable*: a `"p"`
1281/// that is not text leaves [`TraceEvent::peer`] `None`, and the entry then
1282/// belongs in [`TraceEvent::extra`] like any other value the reader could not
1283/// use. Excluding the key unconditionally would delete it instead.
1284fn writes_common_key(peer: Option<&str>, key: &str) -> bool {
1285 REQUIRED_COMMON_KEYS.contains(&key) || (key == "p" && peer.is_some())
1286}
1287
1288/// Whether `key` is written from one of `data`'s own fields — equivalently,
1289/// on an event that was decoded, whether the decode used it. The common keys
1290/// are not this function's business; [`writes_common_key`] answers for those.
1291///
1292/// Deliberately not "does this event type define `key`". The two part company
1293/// on a defined key whose value the decode could not use: SPEC.md treats such
1294/// a key as unrecognised, so its field stays `None` and the entry goes to
1295/// [`TraceEvent::extra`], from where the serializer writes it back unchanged.
1296/// Asking about the type's whole vocabulary instead keeps the key out of
1297/// `extra` while no field holds it either, and merely reading the file deletes
1298/// the value — which is how adding `"ta"`, `"sg"`, `"fri"` and `"g"` to event
1299/// 1 made this reader preserve *less* than while it had never heard of them.
1300///
1301/// The match is exhaustive on purpose: a new variant does not compile until
1302/// its keys are listed, and both ways of getting a list wrong are silent. A
1303/// key left out is written twice, once from the field and once from `extra`,
1304/// and a CBOR map with a duplicate key is malformed; a key wrongly listed is
1305/// dropped from every rewrite.
1306fn writes_variant_key(data: &EventData, key: &str) -> bool {
1307 /// `always` are the keys the variant writes unconditionally; `optional`
1308 /// pairs each of the rest with whether its field holds a value.
1309 fn among(key: &str, always: &[&str], optional: &[(&str, bool)]) -> bool {
1310 always.contains(&key) || optional.iter().any(|&(k, written)| written && k == key)
1311 }
1312
1313 match data {
1314 EventData::ControlMessage { stream_id, raw, .. } => {
1315 among(key, &["d", "mt", "msg"], &[("sid", stream_id.is_some()), ("raw", raw.is_some())])
1316 }
1317 EventData::StreamOpened {
1318 track_alias, subgroup_id, fetch_request_id, group_id, ..
1319 } => among(
1320 key,
1321 &["sid", "d", "st"],
1322 &[
1323 ("ta", track_alias.is_some()),
1324 ("sg", subgroup_id.is_some()),
1325 ("fri", fetch_request_id.is_some()),
1326 ("g", group_id.is_some()),
1327 ],
1328 ),
1329 EventData::StreamClosed { .. } => among(key, &["sid", "ec"], &[]),
1330 EventData::ObjectHeader { .. } => among(key, &["sid", "g", "o", "pp", "os"], &[]),
1331 EventData::ObjectPayload { payload, .. } => {
1332 among(key, &["sid", "g", "o", "sz"], &[("pl", payload.is_some())])
1333 }
1334 EventData::StateChange { .. } => among(key, &["from", "to"], &[]),
1335 EventData::Error { stream_id, kind, raw_len, raw, .. } => among(
1336 key,
1337 &["ec", "reason"],
1338 &[
1339 ("sid", stream_id.is_some()),
1340 ("ek", kind.is_some()),
1341 ("rawlen", raw_len.is_some()),
1342 ("raw", raw.is_some()),
1343 ],
1344 ),
1345 EventData::Annotation { .. } => among(key, &["label", "data"], &[]),
1346 EventData::PeerConnected { endpoint, transport, role, side } => among(
1347 key,
1348 &[],
1349 &[
1350 ("endpoint", endpoint.is_some()),
1351 ("transport", transport.is_some()),
1352 ("role", role.is_some()),
1353 ("side", side.is_some()),
1354 ],
1355 ),
1356 EventData::PeerDisconnected { reason, .. } => {
1357 among(key, &["ec"], &[("reason", reason.is_some())])
1358 }
1359 EventData::SubscriptionDerivation {
1360 trace_id,
1361 namespace,
1362 track_name,
1363 t_downstream_received,
1364 t_upstream_sent,
1365 t_upstream_ok_received,
1366 t_downstream_ok_sent,
1367 ..
1368 } => among(
1369 key,
1370 &["u", "d", "kind"],
1371 &[
1372 ("traceId", trace_id.is_some()),
1373 ("ns", namespace.is_some()),
1374 ("tn", track_name.is_some()),
1375 ("tdr", t_downstream_received.is_some()),
1376 ("tus", t_upstream_sent.is_some()),
1377 ("tuo", t_upstream_ok_received.is_some()),
1378 ("tdo", t_downstream_ok_sent.is_some()),
1379 ],
1380 ),
1381 // An unknown event type writes every key it read straight back out of
1382 // `fields`, so those are the ones an `extra` entry would duplicate.
1383 // Decoding one collects nothing into `extra` — see the `Unknown` arm
1384 // below — but a caller may have attached some by hand.
1385 EventData::Unknown { fields, .. } => fields.iter().any(|(k, _)| k.as_text() == Some(key)),
1386 }
1387}
1388
1389/// Every entry on `pairs` that the event decoded from it does not write back
1390/// out of a field of its own. See [`unrecognised`], which the header uses for
1391/// the same reason.
1392///
1393/// One difference from the header's use of it is worth naming, because it
1394/// decides *which* entry of a duplicate pair survives. A getter here searches
1395/// for the first entry it can **use**, where the header's lookup returns the
1396/// first entry for a key whatever it holds. So on a file repeating a key with
1397/// two different types — `"sid": "x"` and then `"sid": 9` — the field takes
1398/// the second and the walk drops the first. Both were lost before this walk
1399/// existed, and SPEC.md leaves readers free to disagree over which of a
1400/// duplicate pair wins, so nothing may depend on which one it is.
1401fn unrecognised_keys(
1402 pairs: &[(Value, Value)],
1403 peer: Option<&str>,
1404 data: &EventData,
1405) -> Vec<(Value, Value)> {
1406 unrecognised(pairs, |key| writes_common_key(peer, key) || writes_variant_key(data, key))
1407}
1408
1409impl TryFrom<Value> for TraceEvent {
1410 type Error = MoqTraceError;
1411
1412 fn try_from(value: Value) -> Result<Self, MoqTraceError> {
1413 let pairs = match value {
1414 Value::Map(pairs) => pairs,
1415 _ => return Err(MoqTraceError::InvalidEvent("event is not a CBOR map".into())),
1416 };
1417
1418 let seq = require_uint(&pairs, "n")?;
1419 let timestamp = require_int(&pairs, "t")?;
1420 let peer = get_text(&pairs, "p");
1421 let event_type = require_uint(&pairs, "e")?;
1422
1423 let data = match event_type {
1424 EVENT_CONTROL_MESSAGE => EventData::ControlMessage {
1425 direction: require_direction(&pairs, "d")?,
1426 message_type: require_uint(&pairs, "mt")?,
1427 message: get_message(&pairs),
1428 stream_id: get_uint(&pairs, "sid"),
1429 raw: get_bytes(&pairs, "raw"),
1430 },
1431 EVENT_STREAM_OPENED => {
1432 let st_val = require_value(&pairs, "st")?;
1433 EventData::StreamOpened {
1434 stream_id: require_uint(&pairs, "sid")?,
1435 direction: require_direction(&pairs, "d")?,
1436 stream_type: StreamType::from_cbor(&st_val)?,
1437 // Read whatever is usable, including a key outside the
1438 // stream type it is scoped to. A writer must not produce
1439 // one, but "ignore" is the same read-past-and-keep rule
1440 // that governs `extra`: dropping it here would make this
1441 // reader's opinion permanent for every reader downstream.
1442 // A value that is not an unsigned integer is not usable —
1443 // the field stays `None` and the key goes to `extra`,
1444 // which is that same rule one step over. Neither reading
1445 // past a key nor failing to understand its value is a
1446 // licence to delete it.
1447 track_alias: get_uint(&pairs, "ta"),
1448 subgroup_id: get_uint(&pairs, "sg"),
1449 fetch_request_id: get_uint(&pairs, "fri"),
1450 group_id: get_uint(&pairs, "g"),
1451 }
1452 }
1453 EVENT_STREAM_CLOSED => EventData::StreamClosed {
1454 stream_id: require_uint(&pairs, "sid")?,
1455 error_code: require_uint(&pairs, "ec")?,
1456 },
1457 EVENT_OBJECT_HEADER => EventData::ObjectHeader {
1458 stream_id: require_uint(&pairs, "sid")?,
1459 group: require_uint(&pairs, "g")?,
1460 object: require_uint(&pairs, "o")?,
1461 publisher_priority: require_uint(&pairs, "pp")?,
1462 object_status: require_uint(&pairs, "os")?,
1463 },
1464 EVENT_OBJECT_PAYLOAD => EventData::ObjectPayload {
1465 stream_id: require_uint(&pairs, "sid")?,
1466 group: require_uint(&pairs, "g")?,
1467 object: require_uint(&pairs, "o")?,
1468 size: require_uint(&pairs, "sz")?,
1469 payload: get_bytes(&pairs, "pl"),
1470 },
1471 EVENT_STATE_CHANGE => EventData::StateChange {
1472 from: require_text(&pairs, "from")?,
1473 to: require_text(&pairs, "to")?,
1474 },
1475 EVENT_ERROR => EventData::Error {
1476 error_code: require_uint(&pairs, "ec")?,
1477 reason: require_text(&pairs, "reason")?,
1478 // All four optional, and every recording made before they
1479 // existed carries none of them. A `"raw"` longer than the cap
1480 // is read at the length the file gave it: the cap is addressed
1481 // to a recorder building an event from what it observed, and
1482 // re-truncating here would destroy evidence to make somebody
1483 // else's file conform to a rule it was never handed. Report
1484 // the non-conformance if it is worth reporting; do not repair
1485 // it. SPEC.md, Event 6.
1486 stream_id: get_uint(&pairs, "sid"),
1487 kind: get_text(&pairs, "ek").as_deref().map(ErrorKind::parse),
1488 raw_len: get_uint(&pairs, "rawlen"),
1489 raw: get_bytes(&pairs, "raw"),
1490 },
1491 EVENT_ANNOTATION => EventData::Annotation {
1492 label: require_text(&pairs, "label")?,
1493 data: require_value(&pairs, "data")?,
1494 },
1495 EVENT_PEER_CONNECTED => EventData::PeerConnected {
1496 endpoint: get_text(&pairs, "endpoint"),
1497 transport: get_text(&pairs, "transport"),
1498 role: get_text(&pairs, "role").as_deref().map(PeerRole::parse),
1499 side: get_text(&pairs, "side").as_deref().map(Side::parse),
1500 },
1501 EVENT_PEER_DISCONNECTED => EventData::PeerDisconnected {
1502 error_code: require_uint(&pairs, "ec")?,
1503 reason: get_text(&pairs, "reason"),
1504 },
1505 EVENT_SUBSCRIPTION_DERIVATION => {
1506 let upstream = SubscriptionRef::from_value(&require_value(&pairs, "u")?)?;
1507 let downstream = match require_value(&pairs, "d")? {
1508 Value::Array(items) => items
1509 .iter()
1510 .map(SubscriptionRef::from_value)
1511 .collect::<Result<Vec<_>, _>>()?,
1512 _ => {
1513 return Err(MoqTraceError::InvalidEvent(
1514 "'d' (downstream subs) is not a CBOR array".into(),
1515 ))
1516 }
1517 };
1518 EventData::SubscriptionDerivation {
1519 upstream,
1520 downstream,
1521 kind: DerivationKind::parse(&require_text(&pairs, "kind")?),
1522 trace_id: get_trace_id(&pairs)?,
1523 namespace: get_namespace(&pairs),
1524 track_name: get_bytes(&pairs, "tn")
1525 .or_else(|| get_text(&pairs, "tn").map(String::into_bytes)),
1526 t_downstream_received: get_int(&pairs, "tdr"),
1527 t_upstream_sent: get_int(&pairs, "tus"),
1528 t_upstream_ok_received: get_int(&pairs, "tuo"),
1529 t_downstream_ok_sent: get_int(&pairs, "tdo"),
1530 }
1531 }
1532 other => EventData::Unknown {
1533 event_type: other,
1534 // The same walk `unrecognised_keys` does, with only the common
1535 // fields to ask about: an unknown event type writes every
1536 // other key straight back out of here. It had the same defect
1537 // for the same reason — a repeated `"n"` was filtered out by
1538 // name, so the entry `seq` did not take reached neither — and
1539 // this variant collects nothing into `extra`, which makes
1540 // `fields` the only place such an entry can land.
1541 fields: unrecognised(&pairs, |key| writes_common_key(peer.as_deref(), key)),
1542 },
1543 };
1544
1545 let extra = match &data {
1546 // `fields` already holds every key on this event the common
1547 // fields did not consume, a `"p"` this crate could not use among
1548 // them. Collecting them into `extra` as well would write each one
1549 // twice and produce a CBOR map with duplicate keys.
1550 EventData::Unknown { .. } => Vec::new(),
1551 other => unrecognised_keys(&pairs, peer.as_deref(), other),
1552 };
1553 Ok(TraceEvent { seq, timestamp, peer, data, extra })
1554 }
1555}