mumble_server_runtime_shard/
scope.rs

1//! Scopes: the shared-view visibility mechanism.
2//!
3//! A scope is a path in a tree. Every rendered element occupies one; every
4//! connection observes a small set of them. Visibility is a single line:
5//!
6//! > a connection sees an element when one of its observed scopes is
7//! > **comparable** to the element's, meaning one is a prefix of the other.
8//!
9//! Both directions count, and that is what makes the relation useful: a player
10//! at `/g7/t2` sees its ancestors (`/`, `/g7`) *and* its descendants, while a
11//! spectator at `/g7` sees every team without anything being added to the model.
12//! Moving up the tree is how you see more.
13//!
14//! # The closure theorem
15//!
16//! Imposing one rule on rendering - a child channel's scope **extends** its
17//! parent's, a user's scope extends its channel's - makes the property everything
18//! else depends on automatic: *if I see an element, I see what it refers to*.
19//!
20//! With `c` the channel's scope and `u` the user's, `c` a prefix of `u`, an
21//! observer seeing the user through some scope `s` comparable to `u`:
22//!
23//! - `s` is a prefix of `u`: then `c` is also a prefix of `u`, so `s` and `c` are
24//!   two prefixes of the same path, hence comparable;
25//! - `u` is a prefix of `s`: then `c` prefixes `u` prefixes `s`.
26//!
27//! Either way the observer sees the channel. There is no runtime check to write
28//! and no failure mode to chase, provided the builder can only ever narrow -
29//! which is why [`Scope::child`] is the only way to make a new one.
30//!
31//! REF: docs/design/guide-implementation.md 2
32
33/// How deep a scope path may go.
34///
35/// Four is a starting point, not a truth (guide 17.3). It is a hard bound
36/// because [`Scope`] is `Copy` and consulted once per connection per turn: a
37/// heap-allocated path here would put an allocation on the composition path.
38pub const MAX_DEPTH: usize = 4;
39
40/// How many scopes one connection may observe at once.
41pub const MAX_OBSERVED: usize = 4;
42
43/// A position in the visibility tree.
44///
45/// Segments are opaque to Mumble Server Runtime: a flavor puts whatever it wants in them (a
46/// game id, a team id). Two scopes are equal when their paths are equal, so this
47/// derives `Ord` for use as a map key and for canonicalizing a [`ScopeSet`].
48#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
49pub struct Scope {
50    segments: [u32; MAX_DEPTH],
51    depth: u8,
52}
53
54impl Scope {
55    /// The root scope: what everyone who observes anything at all can see.
56    pub const ROOT: Scope = Scope {
57        segments: [0; MAX_DEPTH],
58        depth: 0,
59    };
60
61    /// Extend this scope by one segment.
62    ///
63    /// This is the only constructor beyond [`Scope::ROOT`], and it can only ever
64    /// narrow. That is what makes an incoherent view inexpressible rather than
65    /// merely invalid: there is no free scope parameter anywhere in the builder.
66    ///
67    /// Returns `None` past [`MAX_DEPTH`]. Saturating instead would silently give
68    /// the child its parent's scope, widening what the flavor asked to restrict -
69    /// a privacy leak dressed as a rounding error. Refusing propagates into a
70    /// build error that keeps the previous view (guide 11.7).
71    #[must_use]
72    pub fn child(self, segment: u32) -> Option<Scope> {
73        let depth = usize::from(self.depth);
74        if depth >= MAX_DEPTH {
75            return None;
76        }
77        let mut segments = self.segments;
78        // Bounded by the check above, so the index is in range.
79        segments[depth] = segment;
80        Some(Scope {
81            segments,
82            // `depth < MAX_DEPTH <= u8::MAX`, so this cannot overflow.
83            depth: self.depth.saturating_add(1),
84        })
85    }
86
87    /// How many segments this scope carries. The root has zero.
88    #[must_use]
89    pub fn depth(self) -> usize {
90        usize::from(self.depth)
91    }
92
93    /// Whether this scope is a prefix of `other` (a scope is its own prefix).
94    #[must_use]
95    pub fn is_prefix_of(self, other: Scope) -> bool {
96        let depth = usize::from(self.depth);
97        depth <= usize::from(other.depth) && self.segments[..depth] == other.segments[..depth]
98    }
99
100    /// Whether either scope is a prefix of the other.
101    ///
102    /// Reflexive and symmetric by construction. Deliberately **not** transitive:
103    /// `/g7` is comparable to both `/g7/t2` and `/g7/t3`, which are not
104    /// comparable to each other. That is the point, not a defect.
105    #[must_use]
106    pub fn comparable(self, other: Scope) -> bool {
107        self.is_prefix_of(other) || other.is_prefix_of(self)
108    }
109}
110
111/// A connection dropped more observed scopes than [`MAX_OBSERVED`] allows.
112///
113/// `observation()` runs once per connection per turn, so its result has to stay
114/// a two-word `Copy` value; letting it grow is how the composition cost goes
115/// quadratic again (guide 3.1).
116#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
117#[error("a connection may observe at most {MAX_OBSERVED} scopes, {requested} were given")]
118pub struct TooManyScopes {
119    pub requested: usize,
120}
121
122/// What one connection observes of the shared view.
123///
124/// Canonicalized on construction (sorted, deduplicated) so that equality is set
125/// equality. The shard compares the freshly asked-for observation against the
126/// committed one to decide whether a connection needs the slow path, and a
127/// flavor that returns the same set in a different order must not be mistaken
128/// for one that moved.
129#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
130pub struct ScopeSet {
131    scopes: [Option<Scope>; MAX_OBSERVED],
132}
133
134impl ScopeSet {
135    /// Observes nothing at all. This is the state a connection is attached in,
136    /// which is what makes attaching an ordinary scope change (guide 9.6).
137    pub const NONE: ScopeSet = ScopeSet {
138        scopes: [None; MAX_OBSERVED],
139    };
140
141    /// Build an observation from a slice of scopes.
142    ///
143    /// # Errors
144    ///
145    /// [`TooManyScopes`] when more than [`MAX_OBSERVED`] *distinct* scopes are
146    /// given. Duplicates are free.
147    pub fn new(scopes: &[Scope]) -> Result<ScopeSet, TooManyScopes> {
148        let mut sorted: Vec<Scope> = scopes.to_vec();
149        sorted.sort_unstable();
150        sorted.dedup();
151        if sorted.len() > MAX_OBSERVED {
152            return Err(TooManyScopes {
153                requested: sorted.len(),
154            });
155        }
156
157        let mut set = ScopeSet::NONE;
158        for (slot, scope) in set.scopes.iter_mut().zip(sorted) {
159            *slot = Some(scope);
160        }
161        Ok(set)
162    }
163
164    /// Whether an element at `element` is visible to this connection.
165    #[must_use]
166    pub fn sees(self, element: Scope) -> bool {
167        self.scopes
168            .iter()
169            .flatten()
170            .any(|observed| element.comparable(*observed))
171    }
172
173    /// Whether this connection observes nothing.
174    #[must_use]
175    pub fn is_empty(self) -> bool {
176        self.scopes.iter().all(Option::is_none)
177    }
178
179    /// The observed scopes, in canonical order.
180    pub fn iter(self) -> impl Iterator<Item = Scope> {
181        self.scopes.into_iter().flatten()
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    #![allow(clippy::expect_used)]
188
189    use super::*;
190
191    fn scope(segments: &[u32]) -> Scope {
192        let mut scope = Scope::ROOT;
193        for segment in segments {
194            scope = scope.child(*segment).expect("within MAX_DEPTH");
195        }
196        scope
197    }
198
199    #[test]
200    fn a_child_is_never_a_strict_prefix_of_its_parent() {
201        let parent = scope(&[7]);
202        let child = parent.child(2).expect("within MAX_DEPTH");
203
204        assert!(parent.is_prefix_of(child));
205        assert!(!child.is_prefix_of(parent), "child must not widen");
206        assert_eq!(child.depth(), parent.depth() + 1);
207    }
208
209    #[test]
210    fn comparable_is_reflexive_and_symmetric() {
211        let paths = [
212            scope(&[]),
213            scope(&[7]),
214            scope(&[7, 2]),
215            scope(&[7, 3]),
216            scope(&[8]),
217        ];
218
219        for left in paths {
220            assert!(left.comparable(left), "reflexive");
221            for right in paths {
222                assert_eq!(
223                    left.comparable(right),
224                    right.comparable(left),
225                    "symmetric for {left:?} and {right:?}"
226                );
227            }
228        }
229    }
230
231    #[test]
232    fn siblings_are_not_comparable_but_share_an_ancestor() {
233        let team_two = scope(&[7, 2]);
234        let team_three = scope(&[7, 3]);
235        let game = scope(&[7]);
236
237        assert!(!team_two.comparable(team_three), "teams must be isolated");
238        assert!(game.comparable(team_two));
239        assert!(game.comparable(team_three));
240    }
241
242    #[test]
243    fn observing_higher_sees_strictly_more() {
244        let player = ScopeSet::new(&[scope(&[7, 2])]).expect("one scope");
245        let spectator = ScopeSet::new(&[scope(&[7])]).expect("one scope");
246
247        // The player sees its own team and every ancestor, but not the sibling.
248        assert!(player.sees(scope(&[7, 2])));
249        assert!(player.sees(scope(&[7])));
250        assert!(player.sees(scope(&[])));
251        assert!(!player.sees(scope(&[7, 3])));
252
253        // The spectator sees both teams without anything being added to the model.
254        assert!(spectator.sees(scope(&[7, 2])));
255        assert!(spectator.sees(scope(&[7, 3])));
256        assert!(!spectator.sees(scope(&[8])));
257    }
258
259    #[test]
260    fn narrowing_past_the_bound_refuses_instead_of_widening() {
261        let deepest = scope(&[1, 2, 3, 4]);
262        assert_eq!(deepest.depth(), MAX_DEPTH);
263        assert_eq!(
264            deepest.child(5),
265            None,
266            "saturating here would hand the child its parent's visibility"
267        );
268    }
269
270    #[test]
271    fn an_observation_is_canonical_so_equality_is_set_equality() {
272        let ordered = ScopeSet::new(&[scope(&[7]), scope(&[8])]).expect("two scopes");
273        let reversed = ScopeSet::new(&[scope(&[8]), scope(&[7])]).expect("two scopes");
274        let duplicated =
275            ScopeSet::new(&[scope(&[8]), scope(&[7]), scope(&[8])]).expect("two distinct scopes");
276
277        assert_eq!(ordered, reversed, "order must not look like a move");
278        assert_eq!(ordered, duplicated, "duplicates must not look like a move");
279    }
280
281    #[test]
282    fn too_many_distinct_scopes_are_refused() {
283        let many: Vec<Scope> = (0..=u32::try_from(MAX_OBSERVED).unwrap_or(u32::MAX))
284            .map(|segment| scope(&[segment]))
285            .collect();
286
287        assert_eq!(
288            ScopeSet::new(&many),
289            Err(TooManyScopes {
290                requested: many.len()
291            })
292        );
293    }
294
295    #[test]
296    fn observing_nothing_sees_nothing() {
297        assert!(ScopeSet::NONE.is_empty());
298        assert!(!ScopeSet::NONE.sees(Scope::ROOT));
299    }
300}