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}