mumble_server_runtime_shard/
view.rs

1//! The rendered shard view, and the private overlays layered on top of it.
2//!
3//! A [`ShardView`] holds each fact **once**, whoever ends up seeing it. That is
4//! the whole reason the model is linear: `W` counts facts, while materializing
5//! one view per connection would count copies.
6//!
7//! An [`Overlay`] holds the elements visible to exactly one connection. It is
8//! never journalled and never merged into the shared view: an element is shared
9//! **xor** private (guide 3.4), and [`crate::build`] enforces that.
10
11use std::collections::{BTreeMap, BTreeSet};
12
13use crate::ids::{ActionKey, ChannelId, ChannelKey, Occupant, SessionId};
14use crate::scope::{Scope, ScopeSet};
15
16/// A channel in the shared view.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Channel {
19    /// Stable identity across renders.
20    pub key: ChannelKey,
21    pub id: ChannelId,
22    /// The root is its own parent.
23    pub parent: ChannelId,
24    /// Position in the visibility tree. Extends the parent's.
25    pub scope: Scope,
26    pub name: String,
27    pub position: i32,
28    /// UI hint only: whether this connection may enter. Never a substitute for
29    /// validating an actual join.
30    pub can_enter: bool,
31    /// Whether text may be addressed here. Unlike `can_enter` it has no
32    /// `ChannelState` field of its own: it reaches the client as the
33    /// `TEXT_MESSAGE` bit of a `PermissionQuery` answer, and it is enforced
34    /// again when a message arrives, because a UI hint decides nothing.
35    pub can_text: bool,
36    pub links: BTreeSet<ChannelId>,
37}
38
39/// The boolean user state Mumble carries on `UserState`.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub struct UserFlags {
42    pub mute: bool,
43    pub deaf: bool,
44    pub suppress: bool,
45    pub self_mute: bool,
46    pub self_deaf: bool,
47    pub priority_speaker: bool,
48    pub recording: bool,
49}
50
51/// A user in the shared view, or in one connection's overlay.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct User {
54    /// Stable identity across renders.
55    pub occupant: Occupant,
56    pub session: SessionId,
57    /// The channel the user is shown in. Must be visible wherever the user is.
58    pub channel: ChannelId,
59    /// Position in the visibility tree. Extends the channel's.
60    pub scope: Scope,
61    pub name: String,
62    pub flags: UserFlags,
63}
64
65/// Everything one shard renders, each fact held once.
66#[derive(Debug, Clone, PartialEq, Eq, Default)]
67pub struct ShardView {
68    pub channels: BTreeMap<ChannelId, Channel>,
69    pub users: BTreeMap<SessionId, User>,
70}
71
72impl ShardView {
73    /// The view a connection holds before it has been told anything.
74    #[must_use]
75    pub fn empty() -> ShardView {
76        ShardView::default()
77    }
78
79    /// The part of this view visible to `see`.
80    ///
81    /// Used only on the slow path (guide 9.4), where a connection has to learn a
82    /// whole new subtree. The fast path never materializes a per-connection view
83    /// at all - that is the point of the design.
84    #[must_use]
85    pub fn restrict(&self, see: ScopeSet) -> ShardView {
86        ShardView {
87            channels: self
88                .channels
89                .iter()
90                .filter(|(_, channel)| see.sees(channel.scope))
91                .map(|(id, channel)| (*id, channel.clone()))
92                .collect(),
93            users: self
94                .users
95                .iter()
96                .filter(|(_, user)| see.sees(user.scope))
97                .map(|(session, user)| (*session, user.clone()))
98                .collect(),
99        }
100    }
101
102    /// This view with `overlay` layered on top.
103    ///
104    /// There is nothing to reconcile: shared and private are disjoint by
105    /// construction (guide 3.4), so this is a union, and an element appearing in
106    /// both would be a build error caught long before here.
107    #[must_use]
108    pub fn compose(&self, overlay: &Overlay) -> ShardView {
109        let mut composed = self.clone();
110        composed
111            .channels
112            .extend(overlay.channels.iter().map(|(id, c)| (*id, c.clone())));
113        composed
114            .users
115            .extend(overlay.users.iter().map(|(s, u)| (*s, u.clone())));
116        composed
117    }
118}
119
120/// Elements visible to exactly one connection.
121///
122/// This is the mechanism for individual exceptions - an admin in vanish, a
123/// private channel, a per-observer placement - as opposed to scopes, which
124/// describe groups. A scope with a single observer is an overlay in disguise.
125///
126/// Overlays are recomputed from scratch each turn and diffed against what the
127/// connection was last sent, which is why they never need journalling.
128#[derive(Debug, Clone, PartialEq, Eq, Default)]
129pub struct Overlay {
130    pub channels: BTreeMap<ChannelId, Channel>,
131    pub users: BTreeMap<SessionId, User>,
132}
133
134impl Overlay {
135    #[must_use]
136    pub fn is_empty(&self) -> bool {
137        self.channels.is_empty() && self.users.is_empty()
138    }
139}
140
141/// Where a context action is offered in the client's interface.
142///
143/// A bit set rather than an enum: one action may be offered in several places at
144/// once, which is exactly what the client's three menus do with it.
145///
146/// REF: references/vendored/Mumble.proto : `ContextActionModify.Context`,
147///   `Server = 0x01`, `Channel = 0x02`, `User = 0x04`.
148/// REF: references/mumble/src/mumble/Messages.cpp : `msgContextActionModify`
149///   appends the same action to `qlServerActions`, `qlUserActions` and
150///   `qlChannelActions`, one per bit set.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
152pub struct On(u32);
153
154impl On {
155    /// Offered on the server itself, with nothing selected.
156    pub const SERVER: On = On(0x01);
157    /// Offered when a channel is selected, and told which one.
158    pub const CHANNEL: On = On(0x02);
159    /// Offered when a user is selected, and told which one.
160    pub const USER: On = On(0x04);
161
162    /// Both places at once, and any other combination.
163    #[must_use]
164    pub fn and(self, other: On) -> On {
165        On(self.0 | other.0)
166    }
167
168    /// Whether this action was offered in that place. The invocation is checked
169    /// against it, so a client cannot invoke a user action on a channel.
170    #[must_use]
171    pub fn covers(self, place: On) -> bool {
172        self.0 & place.0 == place.0
173    }
174
175    #[must_use]
176    pub fn bits(self) -> u32 {
177        self.0
178    }
179}
180
181/// One action a flavor offers to one connection.
182#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct Action {
184    pub key: ActionKey,
185    /// What the client writes in the menu. A field, not the identity.
186    pub text: String,
187    pub on: On,
188}
189
190/// The actions offered to one connection.
191///
192/// Private by construction, like an [`Overlay`], and recomputed from scratch
193/// each turn: what a flavor offers may depend on who is asking, which is the
194/// whole point of a button.
195pub type Actions = BTreeMap<ActionKey, Action>;
196
197#[cfg(test)]
198mod tests {
199    #![allow(clippy::expect_used)]
200
201    use super::*;
202    use crate::ids::ConnectionId;
203
204    fn scope(segments: &[u32]) -> Scope {
205        let mut scope = Scope::ROOT;
206        for segment in segments {
207            scope = scope.child(*segment).expect("within MAX_DEPTH");
208        }
209        scope
210    }
211
212    fn channel(id: u32, scope: Scope) -> Channel {
213        Channel {
214            key: ChannelKey(u64::from(id)),
215            id: ChannelId(id),
216            parent: ChannelId::ROOT,
217            scope,
218            name: format!("channel-{id}"),
219            position: 0,
220            can_enter: true,
221            can_text: true,
222            links: BTreeSet::new(),
223        }
224    }
225
226    fn user(session: u32, channel: u32, scope: Scope) -> User {
227        User {
228            occupant: Occupant::Connection(ConnectionId(u64::from(session))),
229            session: SessionId(session),
230            channel: ChannelId(channel),
231            scope,
232            name: format!("user-{session}"),
233            flags: UserFlags::default(),
234        }
235    }
236
237    fn sample() -> ShardView {
238        let mut view = ShardView::empty();
239        view.channels.insert(ChannelId(1), channel(1, scope(&[7])));
240        view.channels
241            .insert(ChannelId(2), channel(2, scope(&[7, 2])));
242        view.channels
243            .insert(ChannelId(3), channel(3, scope(&[7, 3])));
244        view.users
245            .insert(SessionId(10), user(10, 2, scope(&[7, 2])));
246        view.users
247            .insert(SessionId(11), user(11, 3, scope(&[7, 3])));
248        view
249    }
250
251    #[test]
252    fn restricting_keeps_exactly_the_comparable_elements() {
253        let view = sample();
254        let team_two = ScopeSet::new(&[scope(&[7, 2])]).expect("one scope");
255        let restricted = view.restrict(team_two);
256
257        // The team's own channel plus every ancestor channel, and only the
258        // teammate. The sibling team is gone in both maps.
259        assert!(restricted.channels.contains_key(&ChannelId(1)));
260        assert!(restricted.channels.contains_key(&ChannelId(2)));
261        assert!(!restricted.channels.contains_key(&ChannelId(3)));
262        assert!(restricted.users.contains_key(&SessionId(10)));
263        assert!(!restricted.users.contains_key(&SessionId(11)));
264    }
265
266    #[test]
267    fn a_restricted_view_never_strands_a_user_without_its_channel() {
268        // This is the closure theorem observed rather than proved: whatever the
269        // observation, no surviving user references a channel that was filtered
270        // out. There is no runtime check anywhere that makes this true.
271        let view = sample();
272        let observations = [
273            ScopeSet::new(&[scope(&[])]).expect("one scope"),
274            ScopeSet::new(&[scope(&[7])]).expect("one scope"),
275            ScopeSet::new(&[scope(&[7, 2])]).expect("one scope"),
276            ScopeSet::new(&[scope(&[7, 3])]).expect("one scope"),
277            ScopeSet::new(&[scope(&[8])]).expect("one scope"),
278        ];
279
280        for see in observations {
281            let restricted = view.restrict(see);
282            for user in restricted.users.values() {
283                assert!(
284                    restricted.channels.contains_key(&user.channel),
285                    "user {:?} lost its channel under {see:?}",
286                    user.session
287                );
288            }
289        }
290    }
291
292    #[test]
293    fn composing_layers_the_overlay_over_the_shared_view() {
294        let view = sample();
295        let mut overlay = Overlay::default();
296        overlay
297            .users
298            .insert(SessionId(99), user(99, 2, scope(&[7, 2])));
299
300        let composed = view.compose(&overlay);
301        assert!(composed.users.contains_key(&SessionId(99)));
302        assert!(composed.users.contains_key(&SessionId(10)));
303        assert!(
304            !view.users.contains_key(&SessionId(99)),
305            "composing must not mutate the shared view"
306        );
307    }
308}