Skip to main content

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}