1use rand::RngExt;
23use std::collections::HashSet;
24use thiserror::Error;
25
26#[derive(Debug, Error)]
28pub enum IndexError {
29 #[error("no available indices (too many active sessions)")]
30 Exhausted,
31
32 #[error("index {0} not found")]
33 NotFound(u32),
34
35 #[error("index {0} already in use")]
36 AlreadyInUse(u32),
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
43pub struct SessionIndex(u32);
44
45impl SessionIndex {
46 pub fn new(value: u32) -> Self {
48 Self(value)
49 }
50
51 pub fn as_u32(&self) -> u32 {
53 self.0
54 }
55
56 pub fn to_le_bytes(&self) -> [u8; 4] {
58 self.0.to_le_bytes()
59 }
60
61 pub fn from_le_bytes(bytes: [u8; 4]) -> Self {
63 Self(u32::from_le_bytes(bytes))
64 }
65}
66
67impl std::fmt::Display for SessionIndex {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 write!(f, "{:08x}", self.0)
70 }
71}
72
73#[derive(Debug)]
78pub struct IndexAllocator {
79 in_use: HashSet<u32>,
81 max_attempts: usize,
83}
84
85impl IndexAllocator {
86 pub fn new() -> Self {
88 Self {
89 in_use: HashSet::new(),
90 max_attempts: 100,
91 }
92 }
93
94 pub fn with_max_attempts(max_attempts: usize) -> Self {
96 Self {
97 in_use: HashSet::new(),
98 max_attempts,
99 }
100 }
101
102 pub fn allocate(&mut self) -> Result<SessionIndex, IndexError> {
108 let mut rng = rand::rng();
109
110 for _ in 0..self.max_attempts {
111 let candidate = rng.random::<u32>();
112 if !self.in_use.contains(&candidate) {
113 self.in_use.insert(candidate);
114 return Ok(SessionIndex(candidate));
115 }
116 }
117
118 Err(IndexError::Exhausted)
119 }
120
121 pub fn free(&mut self, index: SessionIndex) -> Result<(), IndexError> {
125 if self.in_use.remove(&index.0) {
126 Ok(())
127 } else {
128 Err(IndexError::NotFound(index.0))
129 }
130 }
131
132 pub fn is_allocated(&self, index: SessionIndex) -> bool {
134 self.in_use.contains(&index.0)
135 }
136
137 pub fn count(&self) -> usize {
139 self.in_use.len()
140 }
141
142 pub fn is_empty(&self) -> bool {
144 self.in_use.is_empty()
145 }
146
147 pub fn reserve(&mut self, index: SessionIndex) -> Result<(), IndexError> {
151 if self.in_use.contains(&index.0) {
152 Err(IndexError::AlreadyInUse(index.0))
153 } else {
154 self.in_use.insert(index.0);
155 Ok(())
156 }
157 }
158
159 pub fn clear(&mut self) {
161 self.in_use.clear();
162 }
163}
164
165impl Default for IndexAllocator {
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn test_session_index_roundtrip() {
177 let idx = SessionIndex::new(0x12345678);
178 assert_eq!(idx.as_u32(), 0x12345678);
179
180 let bytes = idx.to_le_bytes();
181 assert_eq!(bytes, [0x78, 0x56, 0x34, 0x12]);
182
183 let restored = SessionIndex::from_le_bytes(bytes);
184 assert_eq!(restored, idx);
185 }
186
187 #[test]
188 fn test_session_index_display() {
189 let idx = SessionIndex::new(0x000000ff);
190 assert_eq!(format!("{}", idx), "000000ff");
191
192 let idx = SessionIndex::new(0xdeadbeef);
193 assert_eq!(format!("{}", idx), "deadbeef");
194 }
195
196 #[test]
197 fn test_allocator_basic() {
198 let mut alloc = IndexAllocator::new();
199 assert!(alloc.is_empty());
200 assert_eq!(alloc.count(), 0);
201
202 let idx1 = alloc.allocate().unwrap();
203 assert!(!alloc.is_empty());
204 assert_eq!(alloc.count(), 1);
205 assert!(alloc.is_allocated(idx1));
206
207 let idx2 = alloc.allocate().unwrap();
208 assert_eq!(alloc.count(), 2);
209 assert!(alloc.is_allocated(idx2));
210 assert_ne!(idx1, idx2);
211
212 alloc.free(idx1).unwrap();
213 assert_eq!(alloc.count(), 1);
214 assert!(!alloc.is_allocated(idx1));
215 assert!(alloc.is_allocated(idx2));
216 }
217
218 #[test]
219 fn test_allocator_free_not_found() {
220 let mut alloc = IndexAllocator::new();
221 let result = alloc.free(SessionIndex::new(12345));
222 assert!(matches!(result, Err(IndexError::NotFound(12345))));
223 }
224
225 #[test]
226 fn test_allocator_reserve() {
227 let mut alloc = IndexAllocator::new();
228
229 let idx = SessionIndex::new(0xdeadbeef);
230 alloc.reserve(idx).unwrap();
231 assert!(alloc.is_allocated(idx));
232
233 let result = alloc.reserve(idx);
235 assert!(matches!(result, Err(IndexError::AlreadyInUse(0xdeadbeef))));
236 }
237
238 #[test]
239 fn test_allocator_uniqueness() {
240 let mut alloc = IndexAllocator::new();
241 let mut indices = Vec::new();
242
243 for _ in 0..1000 {
245 let idx = alloc.allocate().unwrap();
246 assert!(!indices.contains(&idx));
247 indices.push(idx);
248 }
249
250 assert_eq!(alloc.count(), 1000);
251 }
252
253 #[test]
254 fn test_allocator_clear() {
255 let mut alloc = IndexAllocator::new();
256
257 for _ in 0..10 {
258 alloc.allocate().unwrap();
259 }
260 assert_eq!(alloc.count(), 10);
261
262 alloc.clear();
263 assert!(alloc.is_empty());
264 assert_eq!(alloc.count(), 0);
265 }
266
267 #[test]
268 fn test_allocator_reuse_after_free() {
269 let mut alloc = IndexAllocator::new();
270
271 let idx = alloc.allocate().unwrap();
272 alloc.free(idx).unwrap();
273
274 let idx2 = alloc.allocate().unwrap();
277 assert_eq!(alloc.count(), 1);
278
279 if idx != idx2 {
281 alloc.reserve(idx).unwrap();
282 }
283 }
284}