1use std::collections::{HashMap, VecDeque};
29use std::time::{Duration, Instant};
30
31pub const MOST_HOPS: u32 = 6;
35
36pub const CHAIN_WINDOW: Duration = Duration::from_secs(10 * 60);
40
41pub const MOST_PER_WINDOW: usize = 5;
44
45pub const RATE_WINDOW: Duration = Duration::from_secs(60);
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct End {
51 pub tab: u64,
53 pub network: bool,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum Refusal {
60 Network,
62 Chain,
64 Pace,
66 Denied,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum Decision {
73 Allowed,
75 Denied,
77}
78
79#[derive(Debug, Default)]
81pub struct Rules {
82 decisions: HashMap<(u64, u64), Decision>,
84 sent: HashMap<u64, VecDeque<Instant>>,
86 received: HashMap<u64, (u32, Instant)>,
88}
89
90impl Rules {
91 pub fn check(&self, from: End, to: End, now: Instant) -> Result<u32, Refusal> {
101 if !from.network && to.network {
102 return Err(Refusal::Network);
103 }
104 let hop = self.hop(from.tab, now);
105 if hop > MOST_HOPS {
106 return Err(Refusal::Chain);
107 }
108 let recent = self
109 .sent
110 .get(&from.tab)
111 .map_or(0, |times| times.iter().filter(|&&at| now.saturating_duration_since(at) < RATE_WINDOW).count());
112 if recent >= MOST_PER_WINDOW {
113 return Err(Refusal::Pace);
114 }
115 Ok(hop)
116 }
117
118 #[must_use]
121 pub fn hop(&self, tab: u64, now: Instant) -> u32 {
122 match self.received.get(&tab) {
123 Some(&(hop, at)) if now.saturating_duration_since(at) < CHAIN_WINDOW => hop + 1,
124 _ => 1,
125 }
126 }
127
128 #[must_use]
130 pub fn decision(&self, from: u64, to: u64) -> Option<Decision> {
131 self.decisions.get(&(from, to)).copied()
132 }
133
134 pub fn decide(&mut self, from: u64, to: u64, decision: Decision) {
136 self.decisions.insert((from, to), decision);
137 }
138
139 pub fn sent(&mut self, from: u64, now: Instant) {
142 let times = self.sent.entry(from).or_default();
143 while times.front().is_some_and(|&at| now.saturating_duration_since(at) >= RATE_WINDOW) {
144 times.pop_front();
145 }
146 times.push_back(now);
147 }
148
149 pub fn received(&mut self, to: u64, hop: u32, now: Instant) {
152 self.received.insert(to, (hop, now));
153 }
154
155 pub fn new_chain(&mut self, tab: u64) {
159 self.received.remove(&tab);
160 }
161
162 pub fn forget(&mut self, tab: u64) {
165 self.decisions.retain(|&(from, to), _| from != tab && to != tab);
166 self.sent.remove(&tab);
167 self.received.remove(&tab);
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 const ONLINE: bool = true;
176 const OFFLINE: bool = false;
177
178 fn end(tab: u64, network: bool) -> End {
179 End { tab, network }
180 }
181
182 #[test]
183 fn a_tab_without_the_network_never_sends_to_one_with_it() {
184 let rules = Rules::default();
185 let now = Instant::now();
186 assert_eq!(rules.check(end(1, OFFLINE), end(2, ONLINE), now), Err(Refusal::Network));
187 assert_eq!(rules.check(end(1, ONLINE), end(2, OFFLINE), now), Ok(1), "the other way leaks nothing");
188 assert_eq!(rules.check(end(1, OFFLINE), end(2, OFFLINE), now), Ok(1));
189 assert_eq!(rules.check(end(1, ONLINE), end(2, ONLINE), now), Ok(1));
190 }
191
192 #[test]
193 fn the_person_saying_yes_does_not_open_the_network_rule() {
194 let mut rules = Rules::default();
195 rules.decide(1, 2, Decision::Allowed);
196 assert_eq!(rules.check(end(1, OFFLINE), end(2, ONLINE), Instant::now()), Err(Refusal::Network));
197 }
198
199 #[test]
200 fn a_decision_holds_for_one_pair_in_one_direction() {
201 let mut rules = Rules::default();
202 assert_eq!(rules.decision(1, 2), None, "nobody was asked yet");
203 rules.decide(1, 2, Decision::Allowed);
204 rules.decide(3, 2, Decision::Denied);
205 assert_eq!(rules.decision(1, 2), Some(Decision::Allowed));
206 assert_eq!(rules.decision(2, 1), None, "answering is asked about on its own");
207 assert_eq!(rules.decision(3, 2), Some(Decision::Denied));
208 assert_eq!(rules.decision(1, 3), None);
209 }
210
211 #[test]
212 fn a_closed_tab_is_forgotten_with_every_decision_about_it() {
213 let mut rules = Rules::default();
214 rules.decide(1, 2, Decision::Allowed);
215 rules.decide(2, 3, Decision::Allowed);
216 rules.decide(3, 4, Decision::Denied);
217 rules.forget(2);
218 assert_eq!(rules.decision(1, 2), None);
219 assert_eq!(rules.decision(2, 3), None);
220 assert_eq!(rules.decision(3, 4), Some(Decision::Denied));
221 }
222
223 #[test]
224 fn two_tabs_answering_each_other_are_stopped_after_the_longest_chain() {
225 let mut rules = Rules::default();
226 let start = Instant::now();
227 let (a, b) = (end(1, ONLINE), end(2, ONLINE));
228 let mut steps = Vec::new();
229 for turn in 0..10u64 {
230 let now = start + Duration::from_secs(60 * turn);
232 let (from, to) = if turn % 2 == 0 { (a, b) } else { (b, a) };
233 match rules.check(from, to, now) {
234 Ok(hop) => {
235 rules.sent(from.tab, now);
236 rules.received(to.tab, hop, now);
237 steps.push(hop);
238 }
239 Err(refusal) => {
240 assert_eq!(refusal, Refusal::Chain);
241 break;
242 }
243 }
244 }
245 assert_eq!(steps, [1, 2, 3, 4, 5, 6]);
246 }
247
248 #[test]
249 fn a_chain_ends_when_the_tab_was_not_sent_anything_for_a_while() {
250 let mut rules = Rules::default();
251 let now = Instant::now();
252 rules.received(1, MOST_HOPS, now);
253 assert_eq!(rules.check(end(1, ONLINE), end(2, ONLINE), now), Err(Refusal::Chain));
254 assert_eq!(rules.hop(1, now + CHAIN_WINDOW - Duration::from_secs(1)), MOST_HOPS + 1);
255 assert_eq!(rules.check(end(1, ONLINE), end(2, ONLINE), now + CHAIN_WINDOW), Ok(1), "a new task, a new chain");
256 }
257
258 #[test]
259 fn a_tab_sending_in_a_loop_is_stopped_by_its_pace_and_let_go_a_minute_later() {
260 let mut rules = Rules::default();
261 let start = Instant::now();
262 let (from, to) = (end(1, ONLINE), end(2, ONLINE));
263 for second in 0..MOST_PER_WINDOW as u64 {
264 let now = start + Duration::from_secs(second);
265 assert_eq!(rules.check(from, to, now), Ok(1));
266 rules.sent(from.tab, now);
267 }
268 let later = start + Duration::from_secs(10);
269 assert_eq!(rules.check(from, to, later), Err(Refusal::Pace));
270 assert_eq!(rules.check(end(3, ONLINE), to, later), Ok(1), "another sender has its own pace");
271 assert_eq!(rules.check(from, to, start + RATE_WINDOW), Ok(1), "the oldest message left the window");
272 }
273
274 #[test]
275 fn the_rules_are_asked_in_order_network_chain_then_pace() {
276 let mut rules = Rules::default();
277 let now = Instant::now();
278 rules.received(1, MOST_HOPS, now);
279 for _ in 0..MOST_PER_WINDOW {
280 rules.sent(1, now);
281 }
282 assert_eq!(rules.check(end(1, OFFLINE), end(2, ONLINE), now), Err(Refusal::Network));
283 assert_eq!(rules.check(end(1, ONLINE), end(2, ONLINE), now), Err(Refusal::Chain));
284 rules.received(1, 1, now);
285 assert_eq!(rules.check(end(1, ONLINE), end(2, ONLINE), now), Err(Refusal::Pace));
286 }
287}