moqtap_client/track_locations.rs
1//! How far each track's objects have reached, for the drafts that make an
2//! end-of-track object's placement a protocol error.
3//!
4//! Six drafts state it. Draft-11 Section 9.1.1.1 gives the form that drafts 12
5//! and 13 repeat word for word: Object Status 0x4 "Indicates end of Track.
6//! GroupID is either the largest group produced in this track and the ObjectID
7//! is one greater than the largest object produced in that group, or GroupID is
8//! one greater than the largest group produced in this track and the ObjectID is
9//! zero. This status also indicates the last group has ended. An object with
10//! this status that has a Group ID less than any other GroupID, or an ObjectID
11//! less than or equal to the largest in the specified group, is a protocol
12//! error, and the receiver MUST terminate the session."
13//!
14//! Drafts 08, 09 and 10 spell the same prohibition against a status 0x4 that
15//! means "end of Track and Group", and carry a second status beside it — 0x5,
16//! "end of Track" — whose condition is one notch stricter: "An object with this
17//! status that has a Group ID less than or equal to any other Group ID, or an
18//! Object ID other than zero, is a protocol error, and the receiver MUST
19//! terminate the session." Draft-11 merged the two statuses and kept the looser
20//! condition, which is why one record answers both.
21//!
22//! This module holds only the observation. What each draft does about it is its
23//! own close table's business, as it is for every other rule broken on a data
24//! stream.
25//!
26//! # Two numbers per track, and that is not a shortcut
27//!
28//! "The largest object produced in that group" reads like a record of every
29//! group a track has carried, which for a live track is unbounded. It is not
30//! needed, and the reason is the other half of the same sentence.
31//!
32//! The Group ID condition says an end-of-track object may not name a group
33//! behind any the track has carried. So by the time the Object ID condition is
34//! read, "the specified group" is either the largest group the track has
35//! carried or one beyond it — and a group beyond it has carried no objects, so
36//! there is no largest in it to be at or behind. Every group below the largest
37//! is unreachable by the question. Two numbers answer it: the largest Group ID
38//! seen, and the largest Object ID seen *in that group*, which resets whenever
39//! a larger group arrives.
40//!
41//! # Only Normal objects are produced objects
42//!
43//! An object with a status is a statement about objects rather than one of
44//! them, and two of the statuses name an ID one past the end on purpose. Status
45//! 0x3, End of Group, has an "ObjectId ... one greater that the largest object
46//! produced in the group"; status 0x4 has an Object ID one greater again. So a
47//! record that counted an End of Group object as produced would raise the
48//! largest by one and then refuse the very End of Track object the draft
49//! defines — the rule would fire on a conforming track. Only status Normal is
50//! written down.
51//!
52//! # Why the key is the track and not the alias
53//!
54//! The same reason [`crate::forwarding_preference`] gives: an alias is free
55//! again the moment its subscription ends and may then name a different track,
56//! so a record kept against the alias would measure the second track's
57//! end-of-track object against the first track's groups. The alias is resolved
58//! through the endpoint's binding table before anything is written down.
59//!
60//! # Which objects reach this
61//!
62//! Objects a subscriber *receives*, on subgroup streams and in datagrams. The
63//! sentence names the receiver — "the receiver MUST terminate the session" —
64//! which is what separates this from the forwarding-preference rule beside it:
65//! that one names the Original Publisher, so its writers are gated too, and
66//! this one does not.
67//!
68//! # A second rule, and a third number
69//!
70//! Drafts 12 through 20 carry a Malformed Track condition this record is the
71//! right place for as well. Draft-12 Section 2.5: "An Object is received on a
72//! Track whose Group and Object ID are larger than the final Object in the
73//! Track. The final Object in a Track is the Object with Status END_OF_TRACK or
74//! the last Object sent in a FETCH whose response indicated End of Track."
75//! Drafts 13 through 17 carry it word for word; drafts 18, 19 and 20 drop the
76//! two words "on a Track" and change nothing else.
77//!
78//! **It is the mirror of the rule above rather than a widening of it.** That
79//! one asks whether an end-of-track object is behind what the track has
80//! carried, and ends the session over it. This one asks whether an ordinary
81//! object is past where the track ended, and withdraws from the track. Two
82//! rules, two answers, and the two numbers above answer neither of them for the
83//! other: a third is kept, the location the end-of-track object named, written
84//! down once that object has been judged.
85//!
86//! **Larger is the drafts' own comparison and not a reading of the words.**
87//! Every draft that carries the condition carries a Location Structure section
88//! with it, and that section settles the ordering outright — draft-12 Section
89//! 1.3.1 and draft-19 Section 1.4.2 give one Location as below another in the
90//! same words: "A.Group < B.Group || (A.Group == B.Group && A.Object <
91//! B.Object)". Lexicographic, and worth naming because the field-by-field
92//! reading the sentence also admits would let an object in a later group with a
93//! smaller Object ID escape, which is the plainest case of the fault there is.
94//!
95//! **The FETCH half of the definition is out of reach on this path**, and is
96//! not quietly folded into the other half. A fetch's objects arrive on a stream
97//! that opens by naming a Request ID and never a Track Alias, so nothing
98//! measuring objects here knows which track they belong to. What is written
99//! down is the END_OF_TRACK half, and a track whose end was only ever announced
100//! by a fetch response has no final object recorded to be past.
101
102use std::sync::{Arc, Mutex};
103
104use moqtap_codec::types::TrackNamespace;
105
106/// The Group and Object an object names.
107///
108/// Not [`moqtap_codec::types::Location`], which carries the same pair as a
109/// pair of `VarInt`s because it is a field on the wire. This one is compared
110/// and maximised rather than encoded, so it holds the numbers themselves.
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub struct ObjectLocation {
113 /// Group ID.
114 pub group: u64,
115 /// Object ID.
116 pub object: u64,
117}
118
119/// Which of the two conditions an end-of-track status carries.
120///
121/// The distinction is a draft's, not a caller's: drafts 08 through 10 have both
122/// statuses and drafts 11 through 13 have only the first.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum EndOfTrackForm {
125 /// The status whose Group ID names the track's *last* group — 0x4 on all
126 /// six drafts. The Group ID may equal the largest group seen, and the
127 /// Object ID must be past everything produced in it.
128 LastGroup,
129 /// The status whose Group ID names the group *after* the track's last —
130 /// 0x5 on drafts 08, 09 and 10, which draft-11 folded into 0x4. The Group
131 /// ID must be past every group seen.
132 ///
133 /// Its Object-ID-must-be-zero half needs no record and is refused by the
134 /// codec on the one header that carries it.
135 PastLastGroup,
136}
137
138/// Why an end-of-track object is not where the track ended.
139///
140/// Both halves carry what they were measured against, because "this is in the
141/// wrong place" and "this is in the wrong place, and here is the place the
142/// track had already reached" are different reports and only the second can be
143/// read by whoever has to fix it.
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub enum EndOfTrackPlacement {
146 /// The Group ID is behind a group the track has already carried.
147 GroupBehind {
148 /// The largest Group ID the track has carried.
149 largest_group: u64,
150 },
151 /// The Object ID is at or behind the largest object produced in its own
152 /// group.
153 ObjectBehind {
154 /// The largest Object ID produced in that group.
155 largest_object: u64,
156 },
157}
158
159impl std::fmt::Display for EndOfTrackPlacement {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 match self {
162 EndOfTrackPlacement::GroupBehind { largest_group } => {
163 write!(f, "the track has already carried group {largest_group}")
164 }
165 EndOfTrackPlacement::ObjectBehind { largest_object } => {
166 write!(f, "that group's largest object is {largest_object}")
167 }
168 }
169 }
170}
171
172/// What went wrong with an object measured against its track's record.
173///
174/// Two rules meet here and their answers are opposites — one ends the session,
175/// the other gives up a track and leaves the session alone — so they are told
176/// apart at the point of detection rather than at the point of reporting.
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub enum TrackFault {
179 /// An end-of-track object is not where the track ended.
180 EndOfTrackOutOfPlace(EndOfTrackPlacement),
181 /// An object arrived past the track's final object, carrying where the
182 /// track ended so the report can name it.
183 PastFinalObject(ObjectLocation),
184}
185
186/// One track and how far its objects have reached.
187struct TrackRecord {
188 namespace: TrackNamespace,
189 name: Vec<u8>,
190 /// The largest Group ID seen, and the largest Object ID seen in that group.
191 /// `None` until the track has carried an object with status Normal.
192 reached: Option<ObjectLocation>,
193 /// Where the track ended, once an end-of-track object has said so and been
194 /// found to be in a place the track could end at.
195 ///
196 /// `None` on a track still running, and on one whose end-of-track object
197 /// was refused — an object the draft calls a protocol error settles
198 /// nothing, so nothing after it is measured against where it claimed the
199 /// track stopped.
200 final_object: Option<ObjectLocation>,
201}
202
203/// How far each track's objects have reached, so far.
204///
205/// A list rather than a map, and for the same reason the endpoint's own table of
206/// track aliases is one: a session holds a handful of tracks, and
207/// [`TrackNamespace`] has no `Hash`.
208#[derive(Default)]
209pub struct TrackLocations {
210 tracks: Vec<TrackRecord>,
211}
212
213impl TrackLocations {
214 /// An empty record, for a session that has carried no objects yet.
215 pub fn new() -> Self {
216 Self { tracks: Vec::new() }
217 }
218
219 /// Record an object a track produced.
220 ///
221 /// Only status Normal reaches here; see the module documentation for why an
222 /// End of Group object would raise the largest object by one and refuse the
223 /// End of Track object that follows it.
224 pub fn observe(&mut self, namespace: &TrackNamespace, name: &[u8], at: ObjectLocation) {
225 let Some(record) = self.record_mut(namespace, name) else {
226 self.tracks.push(TrackRecord {
227 namespace: namespace.clone(),
228 name: name.to_vec(),
229 reached: Some(at),
230 final_object: None,
231 });
232 return;
233 };
234 record.reached = Some(match record.reached {
235 None => at,
236 // A larger group starts the object count again: what is kept is the
237 // largest object *in the largest group*, and this is a different
238 // group.
239 Some(seen) if at.group > seen.group => at,
240 Some(seen) if at.group == seen.group && at.object > seen.object => at,
241 // A group behind the largest can never be the one an end-of-track
242 // object names, so nothing about it is worth keeping.
243 Some(seen) => seen,
244 });
245 }
246
247 /// Judge where an end-of-track object says the track ended.
248 ///
249 /// `Ok` when the track has carried nothing yet: there is no group for the
250 /// object to be behind, and a receiver that refused it would be enforcing an
251 /// ordering against an empty record.
252 pub fn check_end_of_track(
253 &self,
254 namespace: &TrackNamespace,
255 name: &[u8],
256 at: ObjectLocation,
257 form: EndOfTrackForm,
258 ) -> Result<(), EndOfTrackPlacement> {
259 let Some(reached) = self.record(namespace, name).and_then(|record| record.reached) else {
260 return Ok(());
261 };
262 match form {
263 EndOfTrackForm::PastLastGroup if at.group <= reached.group => {
264 Err(EndOfTrackPlacement::GroupBehind { largest_group: reached.group })
265 }
266 EndOfTrackForm::PastLastGroup => Ok(()),
267 EndOfTrackForm::LastGroup if at.group < reached.group => {
268 Err(EndOfTrackPlacement::GroupBehind { largest_group: reached.group })
269 }
270 // Past the largest group: that group has produced no objects, so
271 // there is no largest in it for this one to be at or behind.
272 EndOfTrackForm::LastGroup if at.group > reached.group => Ok(()),
273 EndOfTrackForm::LastGroup if at.object <= reached.object => {
274 Err(EndOfTrackPlacement::ObjectBehind { largest_object: reached.object })
275 }
276 EndOfTrackForm::LastGroup => Ok(()),
277 }
278 }
279
280 /// Write down where an end-of-track object says the track ended.
281 ///
282 /// Called only after [`Self::check_end_of_track`] has accepted it, so a
283 /// location this record would refuse never becomes the one later objects
284 /// are measured against.
285 ///
286 /// The first one is kept. A second end-of-track object on a track that has
287 /// already ended is itself past the final object on every ordering, so
288 /// letting it move the mark would answer the fault by adopting it.
289 pub fn note_final_object(
290 &mut self,
291 namespace: &TrackNamespace,
292 name: &[u8],
293 at: ObjectLocation,
294 ) {
295 let Some(record) = self.record_mut(namespace, name) else {
296 self.tracks.push(TrackRecord {
297 namespace: namespace.clone(),
298 name: name.to_vec(),
299 reached: None,
300 final_object: Some(at),
301 });
302 return;
303 };
304 if record.final_object.is_none() {
305 record.final_object = Some(at);
306 }
307 }
308
309 /// Judge an ordinary object against where the track ended.
310 ///
311 /// `Ok` on a track no end-of-track object has been seen for: there is no
312 /// final object for this one to be past, and a receiver that refused it
313 /// would be giving up a track for arriving.
314 ///
315 /// The comparison is the drafts' own Location ordering — Group first, and
316 /// the Object only where the Groups are equal — so an object in a later
317 /// group is past the end whatever its Object ID.
318 pub fn check_not_past_final(
319 &self,
320 namespace: &TrackNamespace,
321 name: &[u8],
322 at: ObjectLocation,
323 ) -> Result<(), ObjectLocation> {
324 let Some(final_object) = self.record(namespace, name).and_then(|r| r.final_object) else {
325 return Ok(());
326 };
327 if (at.group, at.object) > (final_object.group, final_object.object) {
328 return Err(final_object);
329 }
330 Ok(())
331 }
332
333 fn record(&self, namespace: &TrackNamespace, name: &[u8]) -> Option<&TrackRecord> {
334 self.tracks.iter().find(|t| t.namespace == *namespace && t.name == name)
335 }
336
337 fn record_mut(&mut self, namespace: &TrackNamespace, name: &[u8]) -> Option<&mut TrackRecord> {
338 self.tracks.iter_mut().find(|t| t.namespace == *namespace && t.name == name)
339 }
340}
341
342/// What an Object Status makes of an object here.
343///
344/// Three answers and not two: a status is not only "ends the track or does
345/// not". End of Group and Object Does Not Exist are statements *about* objects
346/// that neither settle where a track has reached nor test it, and reading them
347/// as either would break the rule in one direction or the other.
348#[derive(Debug, Clone, Copy, PartialEq, Eq)]
349pub enum ObjectRole {
350 /// Status Normal: an object the track produced, and the only kind written
351 /// down.
352 Produced,
353 /// An end-of-track status: the object that settles where the track ended.
354 ///
355 /// `Some(form)` on a draft that states a rule about where one may be
356 /// placed, naming which of the two conditions to judge it by, and `None` on
357 /// a draft that states none. Drafts 08 through 13 are the first; drafts 07
358 /// and 14 through 20 are the second, and
359 /// `a_track_may_end_where_it_has_already_been.rs` asserts the acceptance on
360 /// all seven of them. Either way the object settles where the track ended,
361 /// which is the other record's business and not that rule's.
362 EndsTrack(Option<EndOfTrackForm>),
363 /// Every other status.
364 Neither,
365}
366
367/// One track's record, held by whatever is reading that track's objects.
368///
369/// # Why the stream holds this and not the endpoint
370///
371/// The objects are read one at a time off a stream handle the caller owns, and
372/// that handle has no way back to the session — the same shape trap the close
373/// table has, and the reason `Connection::close_for_data_stream` exists. A
374/// record reachable only from the endpoint would leave the rule enforceable
375/// only by a caller that remembered to ask, which for a conformance crate is
376/// not enforcement. So the endpoint hands one of these to each stream it opens
377/// for a track it can name, the stream measures every object it decodes against
378/// it, and a stream nobody handed one to behaves exactly as it did before.
379///
380/// The clone is of the handle, not of the record: every stream on a session
381/// measures against the same [`TrackLocations`], which is what makes the
382/// largest group a property of the track rather than of one stream.
383#[derive(Clone)]
384pub struct TrackObjects {
385 locations: Arc<Mutex<TrackLocations>>,
386 namespace: TrackNamespace,
387 name: Vec<u8>,
388 /// The Track Alias this handle was resolved from.
389 ///
390 /// Carried for the report only, so it can name the track the way the wire
391 /// did. Nothing is keyed on it — see the module documentation for why the
392 /// record itself is keyed on the track.
393 alias: u64,
394}
395
396impl TrackObjects {
397 /// Bind a record to the track an alias resolved to.
398 pub fn new(
399 locations: Arc<Mutex<TrackLocations>>,
400 namespace: TrackNamespace,
401 name: Vec<u8>,
402 alias: u64,
403 ) -> Self {
404 Self { locations, namespace, name, alias }
405 }
406
407 /// The Track Alias this handle was resolved from.
408 pub fn alias(&self) -> u64 {
409 self.alias
410 }
411
412 /// Record or judge one object, by what its status makes of it.
413 ///
414 /// The one entry point both data paths use, so a subgroup object and a
415 /// datagram cannot come to disagree about which statuses count.
416 pub fn note(&self, at: ObjectLocation, role: ObjectRole) -> Result<(), EndOfTrackPlacement> {
417 match role {
418 ObjectRole::Produced => {
419 self.observe(at);
420 Ok(())
421 }
422 ObjectRole::EndsTrack(form) => {
423 if let Some(form) = form {
424 self.check_end_of_track(at, form)?;
425 }
426 self.note_final_object(at);
427 Ok(())
428 }
429 ObjectRole::Neither => Ok(()),
430 }
431 }
432
433 /// [`Self::note`], and the final-object rule with it, for the drafts that
434 /// state one.
435 ///
436 /// The two rules are checked in the order that gives each the objects it is
437 /// about. Where the track ended is asked first, because an object past the
438 /// end is one this endpoint is giving up the track over and there is
439 /// nothing to be gained by writing it into the record on the way past.
440 ///
441 /// # Every object, and not only the produced ones
442 ///
443 /// The sentence names an Object without qualifying it, and this reads it
444 /// that way: a second end-of-track object naming a later place, or an End
445 /// of Group beyond where the track stopped, is as much a track that carried
446 /// on after its end as an ordinary object would be. That is the opposite of
447 /// how [`TrackLocations::observe`] treats a status, and the two are not in
448 /// tension — a status is not a *produced* object, which is what that record
449 /// counts, but it is still an object *received*, which is what this one
450 /// asks about.
451 pub fn note_with_final_object(
452 &self,
453 at: ObjectLocation,
454 role: ObjectRole,
455 ) -> Result<(), TrackFault> {
456 self.check_not_past_final(at).map_err(TrackFault::PastFinalObject)?;
457 self.note(at, role).map_err(TrackFault::EndOfTrackOutOfPlace)
458 }
459
460 /// The final-object rule on its own, for the drafts that state no rule
461 /// about where an end-of-track object may be placed.
462 ///
463 /// The same rule [`Self::note_with_final_object`] applies, and a different
464 /// set of answers: a draft that judges nothing about placement has one
465 /// fault to report and not two, so its callers are handed the one rather
466 /// than an enum with an arm they cannot reach. Which entry point a draft
467 /// uses is the whole of what it says about the rule beside this one.
468 ///
469 /// An end-of-track object settles where the track ended whatever form its
470 /// status names, because the form only says how to judge a placement and
471 /// this judges none.
472 pub fn note_past_final(
473 &self,
474 at: ObjectLocation,
475 role: ObjectRole,
476 ) -> Result<(), ObjectLocation> {
477 self.check_not_past_final(at)?;
478 match role {
479 ObjectRole::Produced => self.observe(at),
480 ObjectRole::EndsTrack(_) => self.note_final_object(at),
481 ObjectRole::Neither => {}
482 }
483 Ok(())
484 }
485
486 /// Write down where an end-of-track object says the track ended. See
487 /// [`TrackLocations::note_final_object`].
488 pub fn note_final_object(&self, at: ObjectLocation) {
489 self.locked().note_final_object(&self.namespace, &self.name, at);
490 }
491
492 /// Judge an object against where the track ended. See
493 /// [`TrackLocations::check_not_past_final`].
494 pub fn check_not_past_final(&self, at: ObjectLocation) -> Result<(), ObjectLocation> {
495 self.locked().check_not_past_final(&self.namespace, &self.name, at)
496 }
497
498 /// Record an object this track produced. See [`TrackLocations::observe`].
499 pub fn observe(&self, at: ObjectLocation) {
500 self.locked().observe(&self.namespace, &self.name, at);
501 }
502
503 /// Judge an end-of-track object's placement. See
504 /// [`TrackLocations::check_end_of_track`].
505 pub fn check_end_of_track(
506 &self,
507 at: ObjectLocation,
508 form: EndOfTrackForm,
509 ) -> Result<(), EndOfTrackPlacement> {
510 self.locked().check_end_of_track(&self.namespace, &self.name, at, form)
511 }
512
513 /// The record, with a poisoned lock recovered rather than propagated.
514 ///
515 /// Nothing here can leave the record in a state a later reader is misled by:
516 /// every mutation is one field of one entry, and a panic between the read
517 /// and the write leaves the entry as it was.
518 fn locked(&self) -> std::sync::MutexGuard<'_, TrackLocations> {
519 self.locations.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
520 }
521}