ftui_render/
link_registry.rs1#![forbid(unsafe_code)]
2
3use std::collections::HashMap;
19
20const MAX_LINK_ID: u32 = 0x00FF_FFFF;
21
22#[derive(Debug, Clone, Default)]
24pub struct LinkRegistry {
25 links: Vec<Option<String>>,
27 lookup: HashMap<String, u32>,
29 free_list: Vec<u32>,
31}
32
33impl LinkRegistry {
34 pub fn new() -> Self {
36 Self {
37 links: vec![None],
38 lookup: HashMap::new(),
39 free_list: Vec::new(),
40 }
41 }
42
43 pub fn register(&mut self, url: &str) -> u32 {
47 if let Some(&id) = self.lookup.get(url) {
48 return id;
49 }
50
51 let id = if let Some(id) = self.free_list.pop() {
52 id
53 } else {
54 let id = self.links.len() as u32;
55 debug_assert!(id <= MAX_LINK_ID, "link id overflow");
56 if id > MAX_LINK_ID {
57 return 0;
58 }
59 self.links.push(None);
60 id
61 };
62
63 if id == 0 || id > MAX_LINK_ID {
64 return 0;
65 }
66
67 self.links[id as usize] = Some(url.to_string());
68 self.lookup.insert(url.to_string(), id);
69 id
70 }
71
72 pub fn get(&self, id: u32) -> Option<&str> {
74 self.links
75 .get(id as usize)
76 .and_then(|slot| slot.as_ref())
77 .map(|s| s.as_str())
78 }
79
80 pub fn unregister(&mut self, id: u32) {
82 if id == 0 {
83 return;
84 }
85
86 let Some(slot) = self.links.get_mut(id as usize) else {
87 return;
88 };
89
90 if let Some(url) = slot.take() {
91 self.lookup.remove(&url);
92 self.free_list.push(id);
93 }
94 }
95
96 pub fn clear(&mut self) {
98 self.links.clear();
99 self.links.push(None);
100 self.lookup.clear();
101 self.free_list.clear();
102 }
103
104 pub fn len(&self) -> usize {
106 self.links.iter().filter(|slot| slot.is_some()).count()
107 }
108
109 pub fn is_empty(&self) -> bool {
111 self.len() == 0
112 }
113
114 pub fn contains(&self, id: u32) -> bool {
116 self.get(id).is_some()
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn register_and_get() {
126 let mut registry = LinkRegistry::new();
127 let id = registry.register("https://example.com");
128 assert_eq!(registry.get(id), Some("https://example.com"));
129 }
130
131 #[test]
132 fn deduplication() {
133 let mut registry = LinkRegistry::new();
134 let id1 = registry.register("https://example.com");
135 let id2 = registry.register("https://example.com");
136 assert_eq!(id1, id2);
137 assert_eq!(registry.len(), 1);
138 }
139
140 #[test]
141 fn multiple_urls() {
142 let mut registry = LinkRegistry::new();
143 let id1 = registry.register("https://one.com");
144 let id2 = registry.register("https://two.com");
145 assert_ne!(id1, id2);
146 assert_eq!(registry.get(id1), Some("https://one.com"));
147 assert_eq!(registry.get(id2), Some("https://two.com"));
148 }
149
150 #[test]
151 fn unregister_reuses_id() {
152 let mut registry = LinkRegistry::new();
153 let id = registry.register("https://example.com");
154 assert!(registry.contains(id));
155 registry.unregister(id);
156 assert!(!registry.contains(id));
157 let reused = registry.register("https://new.com");
158 assert_eq!(reused, id);
159 }
160
161 #[test]
162 fn clear() {
163 let mut registry = LinkRegistry::new();
164 registry.register("https://one.com");
165 registry.register("https://two.com");
166 assert_eq!(registry.len(), 2);
167 registry.clear();
168 assert!(registry.is_empty());
169 }
170
171 #[test]
174 fn id_zero_is_reserved() {
175 let registry = LinkRegistry::new();
176 assert_eq!(registry.get(0), None);
177 }
178
179 #[test]
180 fn unregister_zero_is_noop() {
181 let mut registry = LinkRegistry::new();
182 registry.register("https://example.com");
183 registry.unregister(0);
184 assert_eq!(registry.len(), 1);
185 }
186
187 #[test]
188 fn get_out_of_bounds_returns_none() {
189 let registry = LinkRegistry::new();
190 assert_eq!(registry.get(999), None);
191 assert_eq!(registry.get(u32::MAX), None);
192 }
193
194 #[test]
195 fn unregister_out_of_bounds_is_safe() {
196 let mut registry = LinkRegistry::new();
197 registry.unregister(999);
198 registry.unregister(u32::MAX);
199 assert!(registry.is_empty());
201 }
202
203 #[test]
204 fn unregister_twice_is_safe() {
205 let mut registry = LinkRegistry::new();
206 let id = registry.register("https://example.com");
207 registry.unregister(id);
208 registry.unregister(id); assert!(registry.is_empty());
210 }
211
212 #[test]
213 fn register_returns_nonzero() {
214 let mut registry = LinkRegistry::new();
215 for i in 0..20 {
216 let id = registry.register(&format!("https://example.com/{i}"));
217 assert_ne!(id, 0, "register must never return id 0");
218 }
219 }
220
221 #[test]
222 fn contains_after_unregister() {
223 let mut registry = LinkRegistry::new();
224 let id = registry.register("https://example.com");
225 assert!(registry.contains(id));
226 registry.unregister(id);
227 assert!(!registry.contains(id));
228 }
229
230 #[test]
231 fn contains_invalid_id() {
232 let registry = LinkRegistry::new();
233 assert!(!registry.contains(0));
234 assert!(!registry.contains(999));
235 }
236
237 #[test]
238 fn dedup_after_unregister_gets_new_id() {
239 let mut registry = LinkRegistry::new();
240 let id1 = registry.register("https://example.com");
241 registry.unregister(id1);
242 let id2 = registry.register("https://example.com");
244 assert_eq!(id2, id1); assert_eq!(registry.get(id2), Some("https://example.com"));
246 assert_eq!(registry.len(), 1);
247 }
248
249 #[test]
250 fn free_list_lifo_order() {
251 let mut registry = LinkRegistry::new();
252 let a = registry.register("https://a.com");
253 let b = registry.register("https://b.com");
254 let c = registry.register("https://c.com");
255
256 registry.unregister(a);
258 registry.unregister(b);
259 registry.unregister(c);
260
261 let new1 = registry.register("https://new1.com");
263 assert_eq!(new1, c);
264 let new2 = registry.register("https://new2.com");
265 assert_eq!(new2, b);
266 let new3 = registry.register("https://new3.com");
267 assert_eq!(new3, a);
268 }
269
270 #[test]
271 fn len_tracks_operations() {
272 let mut registry = LinkRegistry::new();
273 assert_eq!(registry.len(), 0);
274
275 let id1 = registry.register("https://one.com");
276 assert_eq!(registry.len(), 1);
277
278 let id2 = registry.register("https://two.com");
279 assert_eq!(registry.len(), 2);
280
281 registry.register("https://one.com");
283 assert_eq!(registry.len(), 2);
284
285 registry.unregister(id1);
286 assert_eq!(registry.len(), 1);
287
288 registry.unregister(id2);
289 assert_eq!(registry.len(), 0);
290 assert!(registry.is_empty());
291 }
292
293 #[test]
294 fn register_after_clear_works() {
295 let mut registry = LinkRegistry::new();
296 registry.register("https://one.com");
297 registry.register("https://two.com");
298 registry.clear();
299
300 let id = registry.register("https://fresh.com");
301 assert_ne!(id, 0);
302 assert_eq!(registry.get(id), Some("https://fresh.com"));
303 assert_eq!(registry.len(), 1);
304 }
305
306 #[test]
307 fn many_registrations() {
308 let mut registry = LinkRegistry::new();
309 let mut ids = Vec::new();
310 for i in 0..100 {
311 let url = format!("https://example.com/{i}");
312 ids.push(registry.register(&url));
313 }
314 assert_eq!(registry.len(), 100);
315
316 for (i, &id) in ids.iter().enumerate() {
318 assert_ne!(id, 0);
319 let url = format!("https://example.com/{i}");
320 assert_eq!(registry.get(id), Some(url.as_str()));
321 }
322
323 let mut sorted = ids.clone();
325 sorted.sort();
326 sorted.dedup();
327 assert_eq!(sorted.len(), ids.len());
328 }
329
330 mod property {
331 use super::*;
332 use proptest::prelude::*;
333
334 fn arb_url() -> impl Strategy<Value = String> {
335 "[a-z]{3,12}".prop_map(|s| format!("https://{s}.com"))
336 }
337
338 proptest! {
339 #![proptest_config(ProptestConfig::with_cases(256))]
340
341 #[test]
343 fn register_get_roundtrip(url in arb_url()) {
344 let mut registry = LinkRegistry::new();
345 let id = registry.register(&url);
346 prop_assert_ne!(id, 0);
347 prop_assert_eq!(registry.get(id), Some(url.as_str()));
348 }
349
350 #[test]
352 fn dedup_same_id(url in arb_url()) {
353 let mut registry = LinkRegistry::new();
354 let id1 = registry.register(&url);
355 let id2 = registry.register(&url);
356 prop_assert_eq!(id1, id2);
357 prop_assert_eq!(registry.len(), 1);
358 }
359
360 #[test]
362 fn distinct_urls_distinct_ids(count in 2usize..20) {
363 let mut registry = LinkRegistry::new();
364 let mut ids = Vec::new();
365 for i in 0..count {
366 ids.push(registry.register(&format!("https://u{i}.com")));
367 }
368 for i in 0..ids.len() {
369 for j in (i + 1)..ids.len() {
370 prop_assert_ne!(ids[i], ids[j]);
371 }
372 }
373 }
374
375 #[test]
377 fn len_invariant(n_register in 1usize..15, n_unregister in 0usize..15) {
378 let mut registry = LinkRegistry::new();
379 let mut ids = Vec::new();
380 for i in 0..n_register {
381 ids.push(registry.register(&format!("https://r{i}.com")));
382 }
383 prop_assert_eq!(registry.len(), n_register);
384
385 let actual_unreg = n_unregister.min(n_register);
386 for id in &ids[..actual_unreg] {
387 registry.unregister(*id);
388 }
389 prop_assert_eq!(registry.len(), n_register - actual_unreg);
390 }
391
392 #[test]
394 fn slot_reuse(url1 in arb_url(), url2 in arb_url()) {
395 let mut registry = LinkRegistry::new();
396 let id1 = registry.register(&url1);
397 registry.unregister(id1);
398 let id2 = registry.register(&url2);
399 prop_assert_eq!(id1, id2);
400 prop_assert_eq!(registry.get(id2), Some(url2.as_str()));
401 }
402
403 #[test]
405 fn clear_resets(count in 1usize..15) {
406 let mut registry = LinkRegistry::new();
407 let mut ids = Vec::new();
408 for i in 0..count {
409 ids.push(registry.register(&format!("https://c{i}.com")));
410 }
411 registry.clear();
412 prop_assert!(registry.is_empty());
413 for id in &ids {
414 prop_assert_eq!(registry.get(*id), None);
415 }
416 }
417 }
418 }
419}