moqtap_trace/lib.rs
1#![deny(missing_docs)]
2
3//! MoQT session tracing — `.moqtrace` file format.
4//!
5//! Implements the [`.moqtrace` binary format specification](https://github.com/moqtap/moqtap-js/blob/master/packages/trace/SPEC.md)
6//! for recording and replaying MoQT protocol sessions.
7//!
8//! The format uses CBOR encoding and is designed to be streamable,
9//! compact, and cross-language compatible.
10//!
11//! # Versions
12//!
13//! This crate writes format version 2 and reads versions 1 and 2. A version-1
14//! file is a non-segmented version-2 trace carrying none of the keys version 2
15//! added, so nothing recorded before the bump becomes unreadable.
16//!
17//! # Reading a trace you did not write
18//!
19//! Traces outlive the code that reads them, so nothing here rejects a file for
20//! carrying something newer than it knows. An unrecognised event type arrives
21//! as [`EventData::Unknown`](event::EventData::Unknown) with its fields
22//! intact; an unrecognised key on an event type this crate *does* know arrives
23//! in [`TraceEvent::extra`](event::TraceEvent::extra) — as does a key it does
24//! know whose value is not of a type that key can hold, since knowing more
25//! about a key must not mean preserving it less; an unrecognised perspective,
26//! detail level, drop policy or error kind is kept verbatim in the matching
27//! `Other` variant. The enums are `#[non_exhaustive]` for the same reason —
28//! matching on one needs a wildcard arm, and gains a variant without breaking
29//! you.
30//!
31//! The header keeps its keys the same way, in three stores rather than one:
32//! [`TraceHeader::extra`](header::TraceHeader::extra),
33//! [`SegmentInfo::extra`](header::SegmentInfo::extra) and
34//! [`SamplingInfo::extra`](header::SamplingInfo::extra). Each map keeps its
35//! own, because a private key on `"segment"` and a key of the same name at the
36//! top level are different keys. `"custom"` needs no store: every key in it
37//! belongs to whoever wrote the trace, so it is handed back as it was found.
38//!
39//! All of it is kept rather than skipped because reading and writing a trace
40//! back out is a normal thing to do to one — a redaction pass, a filter, a
41//! re-segmentation — and a reader that drops what it did not recognise makes
42//! its own ignorance permanent for every reader downstream of it.
43//!
44//! ## The one shape that does not survive
45//!
46//! CBOR `undefined` (major type 7, value 23 — the byte `0xf7`) reaches this
47//! crate as `null` and is written back as `0xf6`. [`ciborium::Value`] has no
48//! variant for it: ciborium's deserializer routes both `undefined` and `null`
49//! through `visit_none`, so the two arrive identical and a store holds
50//! [`Value::Null`] for either. Nothing above the decoder can tell them apart,
51//! so nothing above it can preserve the difference or report it — seeing it
52//! at all would mean decoding at the `ciborium-ll` layer against a value
53//! model of this crate's own.
54//!
55//! SPEC.md puts that case where it belongs: where a reader cannot observe a
56//! normalisation, the reader is not non-conformant and nothing may depend on
57//! the outcome. `undefined` carries no meaning this format defines and no
58//! conformant writer emits it. The two shapes a decoder folds away that this
59//! crate *can* still act on — an integral float, and a byte string under
60//! RFC 8746's tag 64 — it acts on where the rules apply, at the writer, and
61//! in every value it emits that came out of a file rather than out of a typed
62//! field: the header's three stores and `"custom"`, and on an event
63//! [`TraceEvent::extra`](event::TraceEvent::extra), a control message's
64//! `"msg"`, an annotation's `"data"` and an unknown event type's fields. See
65//! [`TraceHeader::extra`](header::TraceHeader::extra) for what the rules are
66//! and why a reader that preserved either shape all the way out would put the
67//! two implementations back to writing different bytes for one trace.
68//!
69//! # Modules
70//!
71//! - [`header`] — [`TraceHeader`](header::TraceHeader), [`Perspective`](header::Perspective), [`DetailLevel`](header::DetailLevel), [`SegmentInfo`](header::SegmentInfo), [`SamplingInfo`](header::SamplingInfo)
72//! - [`event`] — [`TraceEvent`](event::TraceEvent), [`EventData`](event::EventData), [`Direction`](event::Direction), [`SubscriptionRef`](event::SubscriptionRef)
73//! - [`writer`] — [`MoqTraceWriter`](writer::MoqTraceWriter) for streaming and segmented writes
74//! - [`reader`] — [`MoqTraceReader`](reader::MoqTraceReader) and [`ReadItem`](reader::ReadItem) for streaming and segmented reads
75//! - [`error`] — [`MoqTraceError`](error::MoqTraceError)
76//!
77//! # Re-exports
78//!
79//! [`ciborium::Value`] is re-exported so consumers can build opaque CBOR
80//! values (e.g. for the control message `"msg"` field) without depending
81//! on ciborium directly.
82
83/// The README's examples, compiled as doctests so they cannot rot. The item
84/// exists only under `cfg(doctest)` and is not part of the public API.
85#[doc = include_str!("../README.md")]
86#[cfg(doctest)]
87pub struct ReadmeDoctests;
88
89/// Trace error types.
90pub mod error;
91/// Trace event types.
92pub mod event;
93/// Trace file header types.
94pub mod header;
95/// Streaming `.moqtrace` reader.
96pub mod reader;
97/// Streaming `.moqtrace` writer.
98pub mod writer;
99
100/// Re-export of [`ciborium::Value`] for building opaque CBOR values.
101pub use ciborium::Value;