1use heapless::Deque;
2
3use crate::{RECENT_MIC_CAPACITY, REPLAY_BACKTRACK_SLOTS, REPLAY_STALE_MS};
4
5#[derive(Clone, Debug, PartialEq, Eq)]
7pub enum DupCacheKey {
8 Mic {
10 bytes: [u8; 16],
11 len: u8,
12 route_retry: bool,
13 },
14 Hash32(u32),
17}
18
19#[derive(Clone, Debug)]
21pub struct DuplicateCache<const N: usize = 64> {
22 entries: Deque<(DupCacheKey, u64), N>,
23}
24
25impl<const N: usize> Default for DuplicateCache<N> {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl<const N: usize> DuplicateCache<N> {
32 pub fn new() -> Self {
34 Self {
35 entries: Deque::new(),
36 }
37 }
38
39 pub fn contains(&self, key: &DupCacheKey) -> bool {
41 self.entries.iter().any(|(entry, _)| entry == key)
42 }
43
44 pub fn insert(&mut self, key: DupCacheKey, now_ms: u64) {
46 if self.contains(&key) {
47 return;
48 }
49 if self.entries.is_full() {
50 let _ = self.entries.pop_front();
51 }
52 let _ = self.entries.push_back((key, now_ms));
53 }
54
55 pub fn len(&self) -> usize {
57 self.entries.len()
58 }
59
60 pub fn is_empty(&self) -> bool {
62 self.entries.is_empty()
63 }
64}
65
66#[derive(Clone, Debug, PartialEq, Eq)]
68pub struct RecentMic {
69 pub counter: u32,
71 pub mic: [u8; 16],
73 pub mic_len: u8,
75 pub accepted_ms: u64,
77}
78
79#[derive(Clone, Debug)]
81pub struct ReplayWindow {
82 pub last_accepted: u32,
84 pub last_accepted_time_ms: u64,
86 pub backward_bitmap: u8,
88 pub recent_mics: Deque<RecentMic, RECENT_MIC_CAPACITY>,
90}
91
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub enum ReplayVerdict {
95 Accept,
97 Replay,
99 OutOfWindow,
101 Stale,
103}
104
105impl Default for ReplayWindow {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111impl ReplayWindow {
112 pub fn new() -> Self {
114 Self {
115 last_accepted: 0,
116 last_accepted_time_ms: 0,
117 backward_bitmap: 0,
118 recent_mics: Deque::new(),
119 }
120 }
121
122 pub fn check(&self, counter: u32, mic: &[u8], now_ms: u64) -> ReplayVerdict {
124 if self.last_accepted_time_ms == 0 && self.recent_mics.is_empty() {
125 return ReplayVerdict::Accept;
126 }
127
128 if counter > self.last_accepted {
129 return ReplayVerdict::Accept;
130 }
131
132 if now_ms.saturating_sub(self.last_accepted_time_ms) > REPLAY_STALE_MS {
133 return ReplayVerdict::Stale;
134 }
135
136 let delta = self.last_accepted - counter;
137 if delta > REPLAY_BACKTRACK_SLOTS {
138 return ReplayVerdict::OutOfWindow;
139 }
140
141 let slot_occupied = if delta == 0 {
142 true
143 } else {
144 self.backward_bitmap & (1u8 << (delta - 1)) != 0
145 };
146
147 if !slot_occupied {
148 return ReplayVerdict::Accept;
149 }
150
151 let _ = self.has_matching_recent_mic(counter, mic, now_ms);
152 ReplayVerdict::Replay
153 }
154
155 pub fn accept(&mut self, counter: u32, mic: &[u8], now_ms: u64) {
157 self.prune_recent_mics(now_ms);
158
159 if self.last_accepted_time_ms == 0 && self.recent_mics.is_empty() {
160 self.last_accepted = counter;
161 self.last_accepted_time_ms = now_ms;
162 } else if counter > self.last_accepted {
163 let shift = (counter - self.last_accepted) as usize;
164 self.backward_bitmap = if shift > REPLAY_BACKTRACK_SLOTS as usize {
165 0
166 } else {
167 let shifted = if shift >= u8::BITS as usize {
168 0
169 } else {
170 self.backward_bitmap << shift
171 };
172 shifted | (1u8 << (shift - 1))
173 };
174 self.last_accepted = counter;
175 self.last_accepted_time_ms = now_ms;
176 } else if counter < self.last_accepted {
177 let delta = self.last_accepted - counter;
178 if (1..=REPLAY_BACKTRACK_SLOTS).contains(&delta) {
179 self.backward_bitmap |= 1u8 << (delta - 1);
180 }
181 } else {
182 self.last_accepted_time_ms = now_ms;
183 }
184
185 if let Some((normalized_mic, mic_len)) = normalize_mic(mic) {
186 if self.recent_mics.is_full() {
187 let _ = self.recent_mics.pop_front();
188 }
189 let _ = self.recent_mics.push_back(RecentMic {
190 counter,
191 mic: normalized_mic,
192 mic_len,
193 accepted_ms: now_ms,
194 });
195 }
196 }
197
198 pub fn reset(&mut self, baseline: u32, now_ms: u64) {
200 self.last_accepted = baseline;
201 self.last_accepted_time_ms = now_ms;
202 self.backward_bitmap = 0;
203 self.recent_mics.clear();
204 }
205
206 fn has_matching_recent_mic(&self, counter: u32, mic: &[u8], now_ms: u64) -> bool {
207 let Some((normalized_mic, mic_len)) = normalize_mic(mic) else {
208 return false;
209 };
210
211 self.recent_mics.iter().any(|entry| {
212 entry.counter == counter
213 && now_ms.saturating_sub(entry.accepted_ms) <= REPLAY_STALE_MS
214 && entry.mic_len == mic_len
215 && entry.mic[..mic_len as usize] == normalized_mic[..mic_len as usize]
216 })
217 }
218
219 fn prune_recent_mics(&mut self, now_ms: u64) {
220 while let Some(front) = self.recent_mics.front() {
221 if now_ms.saturating_sub(front.accepted_ms) <= REPLAY_STALE_MS {
222 break;
223 }
224 let _ = self.recent_mics.pop_front();
225 }
226 }
227}
228
229fn normalize_mic(mic: &[u8]) -> Option<([u8; 16], u8)> {
230 if mic.len() > 16 {
231 return None;
232 }
233 let mut out = [0u8; 16];
234 out[..mic.len()].copy_from_slice(mic);
235 Some((out, mic.len() as u8))
236}