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 through draft-20 and
91    /// depends on nothing that moved.
92    ObjectPastFinalObject,
93}
94
95impl std::fmt::Display for MalformedTrackCondition {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        match self {
98            MalformedTrackCondition::MixedForwardingPreference => {
99                f.write_str("an Object was received with a different Forwarding Preference")
100            }
101            MalformedTrackCondition::ObjectPastFinalObject => {
102                f.write_str("an Object was received past the track's final Object")
103            }
104        }
105    }
106}
107
108/// One track that has been found malformed, and what found it.
109struct WithdrawnTrack {
110    namespace: TrackNamespace,
111    name: Vec<u8>,
112    condition: MalformedTrackCondition,
113}
114
115/// The tracks this endpoint has withdrawn from for malformation.
116///
117/// A list rather than a map, for the reason the forwarding-preference record
118/// beside it gives: a session holds a handful of tracks, and this is scanned
119/// only when a condition has already fired.
120#[derive(Default)]
121pub struct MalformedTracks {
122    tracks: Vec<WithdrawnTrack>,
123}
124
125impl MalformedTracks {
126    /// An empty record, for a session that has found nothing malformed.
127    pub fn new() -> Self {
128        Self { tracks: Vec::new() }
129    }
130
131    /// Record that a track has been found malformed.
132    ///
133    /// The first condition is the one kept. A track that goes on being
134    /// malformed after it has been withdrawn from is answered by what has
135    /// already happened to it, and the condition worth reporting afterwards is
136    /// the one that caused that.
137    pub fn note(
138        &mut self,
139        namespace: &TrackNamespace,
140        name: &[u8],
141        condition: MalformedTrackCondition,
142    ) {
143        if self.condition(namespace, name).is_some() {
144            return;
145        }
146        self.tracks.push(WithdrawnTrack {
147            namespace: namespace.clone(),
148            name: name.to_vec(),
149            condition,
150        });
151    }
152
153    /// The condition a track was withdrawn for, or `None` for a track this
154    /// endpoint has found nothing wrong with.
155    pub fn condition(
156        &self,
157        namespace: &TrackNamespace,
158        name: &[u8],
159    ) -> Option<MalformedTrackCondition> {
160        self.tracks
161            .iter()
162            .find(|t| t.namespace == *namespace && t.name == name)
163            .map(|t| t.condition)
164    }
165}