1use std::collections::BTreeMap;
17use std::num::NonZeroUsize;
18
19use kernel::{B3Hash, TimeMs};
20
21use crate::answer::DeviceId;
22
23pub const SESSIONS_MAX: usize = 64;
25pub const SESSION_IDLE_MS: u64 = 60_000;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30enum Hold {
31 Seated(NonZeroUsize),
32 Idle(TimeMs),
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
38struct Session {
39 device: DeviceId,
40 minted: u64,
41 hold: Hold,
42}
43
44impl Session {
45 fn idle(&self) -> bool {
47 match self.hold {
48 Hold::Seated(_) => false,
49 Hold::Idle(_) => true,
50 }
51 }
52
53 fn live(&self, now: TimeMs) -> bool {
55 match self.hold {
56 Hold::Seated(_) => true,
57 Hold::Idle(since) => now.value() < since.value().saturating_add(SESSION_IDLE_MS),
58 }
59 }
60}
61
62#[derive(Debug, Clone, Default, PartialEq, Eq)]
64pub struct Sessions {
65 live: BTreeMap<B3Hash, Session>,
66 minted: u64,
67}
68
69impl Sessions {
70 #[must_use]
72 pub fn holds(&self, presented: &str, now: TimeMs) -> bool {
73 self.live
74 .get(&B3Hash::digest(presented.as_bytes()))
75 .is_some_and(|session| session.live(now))
76 }
77
78 pub(super) fn mint(&mut self, device: DeviceId, token: &str, now: TimeMs) {
82 self.live.retain(|_, session| session.live(now));
83 while self.live.len() >= SESSIONS_MAX {
84 let oldest = |idle_only: bool| {
85 self.live
86 .iter()
87 .filter(|(_, session)| !idle_only || session.idle())
88 .min_by_key(|(_, session)| session.minted)
89 .map(|(digest, _)| *digest)
90 };
91 let Some(evicted) = oldest(true).or_else(|| oldest(false)) else {
92 break;
93 };
94 self.live.remove(&evicted);
95 }
96 self.minted = self.minted.saturating_add(1);
97 self.live.insert(
98 B3Hash::digest(token.as_bytes()),
99 Session {
100 device,
101 minted: self.minted,
102 hold: Hold::Idle(now),
103 },
104 );
105 }
106
107 pub(crate) fn seat(&mut self, presented: &str, now: TimeMs) -> Option<B3Hash> {
111 let digest = B3Hash::digest(presented.as_bytes());
112 let session = self
113 .live
114 .get_mut(&digest)
115 .filter(|session| session.live(now))?;
116 session.hold = Hold::Seated(match session.hold {
117 Hold::Seated(sockets) => sockets.saturating_add(1),
118 Hold::Idle(_) => NonZeroUsize::MIN,
119 });
120 Some(digest)
121 }
122
123 pub(crate) fn unseat(&mut self, digest: &B3Hash, now: TimeMs) {
126 if let Some(session) = self.live.get_mut(digest)
127 && let Hold::Seated(sockets) = session.hold
128 {
129 session.hold = NonZeroUsize::new(sockets.get().saturating_sub(1))
130 .map_or(Hold::Idle(now), Hold::Seated);
131 }
132 }
133
134 pub(crate) fn end(&mut self, digest: &B3Hash) {
136 self.live.remove(digest);
137 }
138
139 pub(super) fn end_device(&mut self, device: &DeviceId) {
141 self.live.retain(|_, session| &session.device != device);
142 }
143}
144
145#[cfg(test)]
146#[allow(
147 clippy::unwrap_used,
148 clippy::indexing_slicing,
149 clippy::arithmetic_side_effects,
150 reason = "test code"
151)]
152mod tests {
153 use std::collections::{BTreeMap, BTreeSet};
154
155 use proptest::prelude::*;
156
157 use super::*;
158 use crate::reception::pairing::{BrowserDoor, DeviceKey, PairedBrowser};
159
160 #[derive(Debug, Clone)]
164 enum Move {
165 Pair(u8),
166 Mint(u8),
167 Seat(usize),
168 Unseat(usize),
169 Forget(u8),
170 Wait(u64),
171 }
172
173 fn a_move() -> impl Strategy<Value = Move> {
174 prop_oneof![
175 (0u8..3).prop_map(Move::Pair),
176 (0u8..3).prop_map(Move::Mint),
177 (0usize..8).prop_map(Move::Seat),
178 (0usize..8).prop_map(Move::Unseat),
179 (0u8..3).prop_map(Move::Forget),
180 (0u64..2 * SESSION_IDLE_MS).prop_map(Move::Wait),
181 ]
182 }
183
184 struct Minted {
186 token: String,
187 device: DeviceId,
188 seats: usize,
189 idle_since: u64,
190 lapsed: bool,
191 }
192
193 proptest! {
194 #[test]
202 fn forgotten_and_lapsed_tokens_stay_ended_and_seated_ones_stay_good(
203 moves in proptest::collection::vec(a_move(), 0..48),
204 ) {
205 let mut door = BrowserDoor::new(Vec::new(), [0; 32]);
206 let mut now = 0u64;
207 let mut minted: Vec<Minted> = Vec::new();
208 let mut devices: BTreeMap<u8, DeviceId> = BTreeMap::new();
209 let mut forgotten: BTreeSet<DeviceId> = BTreeSet::new();
210 for (step, each) in moves.into_iter().enumerate() {
211 let at = TimeMs::new(now);
212 match each {
213 Move::Pair(which) => {
214 let line = PairedBrowser::new(DeviceKey::parse(&format!("{which:02x}").repeat(32)).unwrap(), "browser".to_owned(), at);
215 let device = door.pair(line);
216 forgotten.remove(&device);
217 devices.insert(which, device);
218 }
219 Move::Mint(which) => {
220 let Some(device) = devices.get(&which) else { continue };
221 let entropy = [u8::try_from(step).unwrap(); 32];
222 if let Some(token) = door.open_session(device, entropy, at) {
223 minted.push(Minted { token, device: device.clone(), seats: 0, idle_since: now, lapsed: false });
224 }
225 }
226 Move::Seat(pick) => {
227 let Some(token) = minted.get_mut(pick) else { continue };
228 if door.sessions_mut().seat(&token.token, at).is_some() {
229 token.seats += 1;
230 }
231 }
232 Move::Unseat(pick) => {
233 let Some(token) = minted.get_mut(pick) else { continue };
234 if token.seats > 0 {
235 door.sessions_mut().unseat(&B3Hash::digest(token.token.as_bytes()), at);
236 token.seats -= 1;
237 token.idle_since = now;
238 }
239 }
240 Move::Forget(which) => {
241 let Some(device) = devices.get(&which) else { continue };
242 door.forget(device);
243 forgotten.insert(device.clone());
244 for token in minted.iter_mut().filter(|token| &token.device == device) {
245 token.seats = 0;
246 }
247 }
248 Move::Wait(ms) => now += ms,
249 }
250 let at = TimeMs::new(now);
251 for token in &mut minted {
252 let admits = door.sessions().holds(&token.token, at);
253 if forgotten.contains(&token.device) {
254 prop_assert!(!admits, "a token of a forgotten device admits");
255 }
256 if token.lapsed {
257 prop_assert!(!admits, "a token admits again after it stopped");
258 }
259 if token.seats > 0 {
260 prop_assert!(admits, "a token a socket holds stopped admitting");
261 } else if now >= token.idle_since + SESSION_IDLE_MS {
262 prop_assert!(!admits, "an idle token outlived its allowance");
263 }
264 token.lapsed = !admits;
265 }
266 }
267 }
268 }
269}