Skip to main content

moqtap_client/
malformed_tracks.rs

1//! What a Malformed Track is in this crate, and what detecting one costs.
2//!
3//! Draft-12 Section 2.5 opens the definition: "There are multiple ways a
4//! publisher can transmit a Track that does not conform to MoQT constraints.
5//! Such a Track is considered malformed. Some example conditions that
6//! constitute a malformed track when detected by a receiver include:" — and
7//! then eleven bullets, closing with "The above list of conditions is not
8//! considered exhaustive."
9//!
10//! The list is examples. What is not an example is the answer, which the same
11//! section states once for all of them: "When a subscriber detects a Malformed
12//! Track, it MUST UNSUBSCRIBE from the Track and SHOULD deliver an error to
13//! the application." That sentence is the whole of what this module exists
14//! for. A condition is detected somewhere in the data plane; the track it
15//! names is withdrawn; the caller is told which condition it was.
16//!
17//! # A track is withdrawn, and the session is not
18//!
19//! Nothing here ends a session. The answer is an UNSUBSCRIBE and an error
20//! handed up, and the connection stays exactly where it was — which is why
21//! this record and the close table are separate things, and why no condition
22//! recorded here reaches [`crate::draft12::endpoint::EndpointError`]'s session
23//! error codes. A malformed track costs one track.
24//!
25//! # The answer is given once, and this record is not what makes it once
26//!
27//! A publisher that mixes one track's framing will usually go on mixing it,
28//! and the draft asks for an UNSUBSCRIBE rather than one per offending
29//! object. What keeps it to one is the subscription itself: the first
30//! withdrawal ends every request the track was arriving through, and a
31//! request that has ended has no second UNSUBSCRIBE in it.
32//!
33//! So this record does not gate the withdrawal, and the difference matters in
34//! one case. An application that subscribes to the same track *again* is
35//! opening a request that has never been withdrawn from, and a publisher that
36//! mixes its framing again has broken the sentence again. A record that
37//! refused the second withdrawal because it recognised the track would leave
38//! that subscription running over traffic the draft says to give up on —
39//! remembering a track and refusing to act on it are not the same thing.
40//!
41//! # Why the key is the track and not the alias
42//!
43//! The same reason [`crate::forwarding_preference`] gives, and it bites harder
44//! here. An alias is free again the moment its subscription ends and may then
45//! name a different track — so a withdrawal recorded against the alias would
46//! make the *next* track's first malformation look like one already answered,
47//! and that track would never be withdrawn at all. The alias is resolved
48//! through the endpoint's binding table before anything is written down.
49//!
50//! # What "subscriber" excludes
51//!
52//! The sentence names a subscriber, so only the paths on which this endpoint
53//! *receives* a track owe the withdrawal. The two writing paths detect the
54//! same condition — an endpoint is the Original Publisher there and the rule
55//! that binds it is a rule about publishing — and they answer it by refusing
56//! to write, which withdraws nothing because there is nothing to withdraw.
57//!
58//! The section's other sentence is a relay's: "If a relay detects a Malformed
59//! Track, it MUST immediately terminate downstream subscriptions with
60//! SUBSCRIBE_DONE with Status Code Malformed Track." This crate is a client.
61//! It has no downstream subscriptions to terminate, so that half is not
62//! unimplemented here so much as inapplicable; a relay built on top of this
63//! has the condition reported to it and its own downstream to answer for.
64
65use moqtap_codec::types::TrackNamespace;
66
67/// Which condition of the list made a track malformed.
68///
69/// One variant per condition this crate can actually detect, which is fewer
70/// than the drafts list. A condition nothing observes would be a name with no
71/// call site, and the list is explicitly not exhaustive in either direction.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum MalformedTrackCondition {
74    /// "An Object is received with a different Forwarding Preference than
75    /// previously observed from the same Track."
76    ///
77    /// Detected by [`crate::forwarding_preference`], which holds the framing
78    /// each track's first object settled on.
79    MixedForwardingPreference,
80    /// "An Object is received on a Track whose Group and Object ID are larger
81    /// than the final Object in the Track."
82    ///
83    /// Detected by [`crate::track_locations`], which writes down where an
84    /// end-of-track object said the track stopped and measures what arrives
85    /// afterwards against it.
86    ///
87    /// It reaches drafts the condition beside it does not. Draft-16 made the
88    /// Forwarding Preference a property of an Object rather than of a Track, so
89    /// there is nothing on that draft for a mixed-framing record to contradict;
90    /// this condition is stated unchanged from draft-12 on and
91    /// depends on nothing that moved.
92    ObjectPastFinalObject,
93    /// "An Object is received after it has been recorded as not existing, or
94    /// is signalled as not existing after it was received" (draft-23 Section
95    /// 12.1).
96    ///
97    /// Detected by [`crate::track_locations`], which records the Objects a
98    /// track has carried and the ranges signalled as not existing.
99    ExistenceContradicted,
100    /// "A track with a Group that contains more than one Object with different
101    /// values for Prior Group ID Gap or has a Prior Group ID Gap larger than
102    /// the Group ID is considered malformed" (draft-12 Section 10.1,
103    /// draft-13 Section 10.1).
104    ///
105    /// Detected by the codec's `SubgroupGaps`, which a subgroup stream reader
106    /// holds for one stream.
107    PriorGroupIdGap,
108    /// A Prior Group ID Gap or Prior Object ID Gap condition, on drafts 14-23:
109    /// draft-14 Section 11.1, draft-14 Section 11.3, draft-15 Section 11.1,
110    /// draft-15 Section 11.2, draft-16 Section 11.3, draft-16 Section 11.4,
111    /// draft-17 Section 11.7, draft-17 Section 11.8, draft-18 Section 12.8,
112    /// draft-18 Section 12.9, draft-19 Section 12.8, draft-19 Section 12.9,
113    /// draft-20 Section 12.8, draft-20 Section 12.9, draft-21 Section 10.8,
114    /// draft-21 Section 10.9, draft-22 Section 10.8, draft-22 Section 10.9,
115    /// draft-23 Section 10.8 and draft-23 Section 10.9.
116    ///
117    /// Detected by the codec's subgroup, datagram and fetch readers, whose
118    /// `MalformedTrack` error does not say which of the two gaps it was.
119    PriorGap,
120    /// "An Object contains an Immutable Extensions header that contains another
121    /// Immutable Extensions key" (Immutable Properties from draft-17) or "A
122    /// Key-Value-Pair cannot be parsed", on drafts 14-23: draft-14 Section 11.2,
123    /// draft-15 Section 11.1, draft-16 Section 11.2, draft-17 Section 11.6,
124    /// draft-18 Section 12.7, draft-19 Section 12.7, draft-20 Section 12.7,
125    /// draft-21 Section 10.7, draft-22 Section 10.7 and draft-23 Section 10.7.
126    ///
127    /// Detected by the codec's subgroup, datagram and fetch readers, whose
128    /// `MalformedObjectProperties` error does not say which of the two it was.
129    MalformedPropertyBlock,
130}
131
132impl std::fmt::Display for MalformedTrackCondition {
133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        match self {
135            MalformedTrackCondition::MixedForwardingPreference => {
136                f.write_str("an Object was received with a different Forwarding Preference")
137            }
138            MalformedTrackCondition::ObjectPastFinalObject => {
139                f.write_str("an Object was received past the track's final Object")
140            }
141            MalformedTrackCondition::ExistenceContradicted => {
142                f.write_str("an Object was both received and signalled as not existing")
143            }
144            MalformedTrackCondition::PriorGroupIdGap => {
145                f.write_str("an Object carried a Prior Group ID Gap its Group cannot have")
146            }
147            MalformedTrackCondition::PriorGap => {
148                f.write_str("an Object carried a Prior Group ID Gap or Prior Object ID Gap its Track cannot have")
149            }
150            MalformedTrackCondition::MalformedPropertyBlock => {
151                f.write_str(
152                    "an Object carried an extension or property block its draft makes malformed (a Key-Value-Pair that cannot be parsed, or Immutable nested in Immutable)",
153                )
154            }
155        }
156    }
157}
158
159/// One track that has been found malformed, and what found it.
160struct WithdrawnTrack {
161    namespace: TrackNamespace,
162    name: Vec<u8>,
163    condition: MalformedTrackCondition,
164}
165
166/// The tracks this endpoint has withdrawn from for malformation.
167///
168/// A list rather than a map, for the reason the forwarding-preference record
169/// beside it gives: a session holds a handful of tracks, and this is scanned
170/// only when a condition has already fired.
171#[derive(Default)]
172pub struct MalformedTracks {
173    tracks: Vec<WithdrawnTrack>,
174}
175
176impl MalformedTracks {
177    /// An empty record, for a session that has found nothing malformed.
178    pub fn new() -> Self {
179        Self { tracks: Vec::new() }
180    }
181
182    /// Record that a track has been found malformed.
183    ///
184    /// The first condition is the one kept. A track that goes on being
185    /// malformed after it has been withdrawn from is answered by what has
186    /// already happened to it, and the condition worth reporting afterwards is
187    /// the one that caused that.
188    pub fn note(
189        &mut self,
190        namespace: &TrackNamespace,
191        name: &[u8],
192        condition: MalformedTrackCondition,
193    ) {
194        if self.condition(namespace, name).is_some() {
195            return;
196        }
197        self.tracks.push(WithdrawnTrack {
198            namespace: namespace.clone(),
199            name: name.to_vec(),
200            condition,
201        });
202    }
203
204    /// The condition a track was withdrawn for, or `None` for a track this
205    /// endpoint has found nothing wrong with.
206    pub fn condition(
207        &self,
208        namespace: &TrackNamespace,
209        name: &[u8],
210    ) -> Option<MalformedTrackCondition> {
211        self.tracks
212            .iter()
213            .find(|t| t.namespace == *namespace && t.name == name)
214            .map(|t| t.condition)
215    }
216}