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}