1#![forbid(unsafe_code)]
2
3use ahash::AHashMap;
19
20const MAX_LINK_ID: u32 = 0x00FF_FFFF;
21
22#[derive(Debug, Clone, Default)]
24pub struct LinkRegistry {
25 links: Vec<Option<String>>,
27 lookup: AHashMap<String, u32>,
29 free_list: Vec<u32>,
31}
32
33impl LinkRegistry {
34 pub fn new() -> Self {
36 Self {
37 links: vec![None],
38 lookup: AHashMap::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 #[must_use]
74 pub fn get(&self, id: u32) -> Option<&str> {
75 self.links
76 .get(id as usize)
77 .and_then(|slot| slot.as_ref())
78 .map(|s| s.as_str())
79 }
80
81 pub fn unregister(&mut self, id: u32) {
83 if id == 0 {
84 return;
85 }
86
87 let Some(slot) = self.links.get_mut(id as usize) else {
88 return;
89 };
90
91 if let Some(url) = slot.take() {
92 self.lookup.remove(&url);
93 self.free_list.push(id);
94 }
95 }
96
97 pub fn clear(&mut self) {
99 self.links.clear();
100 self.links.push(None);
101 self.lookup.clear();
102 self.free_list.clear();
103 }
104
105 pub fn len(&self) -> usize {
107 self.links.iter().filter(|slot| slot.is_some()).count()
108 }
109
110 #[inline]
112 pub fn is_empty(&self) -> bool {
113 self.len() == 0
114 }
115
116 #[inline]
118 pub fn contains(&self, id: u32) -> bool {
119 self.get(id).is_some()
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn register_and_get() {
129 let mut registry = LinkRegistry::new();
130 let id = registry.register("https://example.com");
131 assert_eq!(registry.get(id), Some("https://example.com"));
132 }
133
134 #[test]
135 fn deduplication() {
136 let mut registry = LinkRegistry::new();
137 let id1 = registry.register("https://example.com");
138 let id2 = registry.register("https://example.com");
139 assert_eq!(id1, id2);
140 assert_eq!(registry.len(), 1);
141 }
142
143 #[test]
144 fn multiple_urls() {
145 let mut registry = LinkRegistry::new();
146 let id1 = registry.register("https://one.com");
147 let id2 = registry.register("https://two.com");
148 assert_ne!(id1, id2);
149 assert_eq!(registry.get(id1), Some("https://one.com"));
150 assert_eq!(registry.get(id2), Some("https://two.com"));
151 }
152
153 #[test]
154 fn unregister_reuses_id() {
155 let mut registry = LinkRegistry::new();
156 let id = registry.register("https://example.com");
157 assert!(registry.contains(id));
158 registry.unregister(id);
159 assert!(!registry.contains(id));
160 let reused = registry.register("https://new.com");
161 assert_eq!(reused, id);
162 }
163
164 #[test]
165 fn clear() {
166 let mut registry = LinkRegistry::new();
167 registry.register("https://one.com");
168 registry.register("https://two.com");
169 assert_eq!(registry.len(), 2);
170 registry.clear();
171 assert!(registry.is_empty());
172 }
173
174 #[test]
177 fn id_zero_is_reserved() {
178 let registry = LinkRegistry::new();
179 assert_eq!(registry.get(0), None);
180 }
181
182 #[test]
183 fn unregister_zero_is_noop() {
184 let mut registry = LinkRegistry::new();
185 registry.register("https://example.com");
186 registry.unregister(0);
187 assert_eq!(registry.len(), 1);
188 }
189
190 #[test]
191 fn get_out_of_bounds_returns_none() {
192 let registry = LinkRegistry::new();
193 assert_eq!(registry.get(999), None);
194 assert_eq!(registry.get(u32::MAX), None);
195 }
196
197 #[test]
198 fn unregister_out_of_bounds_is_safe() {
199 let mut registry = LinkRegistry::new();
200 registry.unregister(999);
201 registry.unregister(u32::MAX);
202 assert!(registry.is_empty());
204 }
205
206 #[test]
207 fn unregister_twice_is_safe() {
208 let mut registry = LinkRegistry::new();
209 let id = registry.register("https://example.com");
210 registry.unregister(id);
211 registry.unregister(id); assert!(registry.is_empty());
213 }
214
215 #[test]
216 fn register_returns_nonzero() {
217 let mut registry = LinkRegistry::new();
218 for i in 0..20 {
219 let id = registry.register(&format!("https://example.com/{i}"));
220 assert_ne!(id, 0, "register must never return id 0");
221 }
222 }
223
224 #[test]
225 fn contains_after_unregister() {
226 let mut registry = LinkRegistry::new();
227 let id = registry.register("https://example.com");
228 assert!(registry.contains(id));
229 registry.unregister(id);
230 assert!(!registry.contains(id));
231 }
232
233 #[test]
234 fn contains_invalid_id() {
235 let registry = LinkRegistry::new();
236 assert!(!registry.contains(0));
237 assert!(!registry.contains(999));
238 }
239
240 #[test]
241 fn dedup_after_unregister_gets_new_id() {
242 let mut registry = LinkRegistry::new();
243 let id1 = registry.register("https://example.com");
244 registry.unregister(id1);
245 let id2 = registry.register("https://example.com");
247 assert_eq!(id2, id1); assert_eq!(registry.get(id2), Some("https://example.com"));
249 assert_eq!(registry.len(), 1);
250 }
251
252 #[test]
253 fn free_list_lifo_order() {
254 let mut registry = LinkRegistry::new();
255 let a = registry.register("https://a.com");
256 let b = registry.register("https://b.com");
257 let c = registry.register("https://c.com");
258
259 registry.unregister(a);
261 registry.unregister(b);
262 registry.unregister(c);
263
264 let new1 = registry.register("https://new1.com");
266 assert_eq!(new1, c);
267 let new2 = registry.register("https://new2.com");
268 assert_eq!(new2, b);
269 let new3 = registry.register("https://new3.com");
270 assert_eq!(new3, a);
271 }
272
273 #[test]
274 fn len_tracks_operations() {
275 let mut registry = LinkRegistry::new();
276 assert_eq!(registry.len(), 0);
277
278 let id1 = registry.register("https://one.com");
279 assert_eq!(registry.len(), 1);
280
281 let id2 = registry.register("https://two.com");
282 assert_eq!(registry.len(), 2);
283
284 registry.register("https://one.com");
286 assert_eq!(registry.len(), 2);
287
288 registry.unregister(id1);
289 assert_eq!(registry.len(), 1);
290
291 registry.unregister(id2);
292 assert_eq!(registry.len(), 0);
293 assert!(registry.is_empty());
294 }
295
296 #[test]
297 fn register_after_clear_works() {
298 let mut registry = LinkRegistry::new();
299 registry.register("https://one.com");
300 registry.register("https://two.com");
301 registry.clear();
302
303 let id = registry.register("https://fresh.com");
304 assert_ne!(id, 0);
305 assert_eq!(registry.get(id), Some("https://fresh.com"));
306 assert_eq!(registry.len(), 1);
307 }
308
309 #[test]
310 fn many_registrations() {
311 let mut registry = LinkRegistry::new();
312 let mut ids = Vec::new();
313 for i in 0..100 {
314 let url = format!("https://example.com/{i}");
315 ids.push(registry.register(&url));
316 }
317 assert_eq!(registry.len(), 100);
318
319 for (i, &id) in ids.iter().enumerate() {
321 assert_ne!(id, 0);
322 let url = format!("https://example.com/{i}");
323 assert_eq!(registry.get(id), Some(url.as_str()));
324 }
325
326 let mut sorted = ids.clone();
328 sorted.sort();
329 sorted.dedup();
330 assert_eq!(sorted.len(), ids.len());
331 }
332
333 #[test]
338 fn default_trait_creates_empty_registry() {
339 let registry = LinkRegistry::default();
340 assert!(registry.is_empty());
341 assert_eq!(registry.len(), 0);
342 assert_eq!(registry.get(0), None);
343 }
344
345 #[test]
346 fn clone_independence() {
347 let mut original = LinkRegistry::new();
348 let id = original.register("https://example.com");
349 let mut cloned = original.clone();
350
351 cloned.unregister(id);
353 assert!(!cloned.contains(id));
354
355 assert!(original.contains(id));
357 assert_eq!(original.get(id), Some("https://example.com"));
358 }
359
360 #[test]
361 fn debug_formatting() {
362 let mut registry = LinkRegistry::new();
363 registry.register("https://example.com");
364 let dbg = format!("{:?}", registry);
365 assert!(dbg.contains("LinkRegistry"));
366 assert!(dbg.contains("example.com"));
367 }
368
369 #[test]
370 fn register_empty_url() {
371 let mut registry = LinkRegistry::new();
372 let id = registry.register("");
373 assert_ne!(id, 0);
374 assert_eq!(registry.get(id), Some(""));
375 assert_eq!(registry.len(), 1);
376 }
377
378 #[test]
379 fn register_url_with_special_chars() {
380 let mut registry = LinkRegistry::new();
381 let url = "https://example.com/path?q=hello world&foo=bar#section";
382 let id = registry.register(url);
383 assert_eq!(registry.get(id), Some(url));
384 }
385
386 #[test]
387 fn register_url_with_unicode() {
388 let mut registry = LinkRegistry::new();
389 let url = "https://例え.jp/日本語";
390 let id = registry.register(url);
391 assert_eq!(registry.get(id), Some(url));
392 }
393
394 #[test]
395 fn register_very_long_url() {
396 let mut registry = LinkRegistry::new();
397 let url = format!("https://example.com/{}", "a".repeat(10_000));
398 let id = registry.register(&url);
399 assert_eq!(registry.get(id), Some(url.as_str()));
400 }
401
402 #[test]
403 fn is_empty_on_fresh_registry() {
404 let registry = LinkRegistry::new();
405 assert!(registry.is_empty());
406 assert_eq!(registry.len(), 0);
407 }
408
409 #[test]
410 fn is_empty_after_register_unregister() {
411 let mut registry = LinkRegistry::new();
412 let id = registry.register("https://test.com");
413 assert!(!registry.is_empty());
414 registry.unregister(id);
415 assert!(registry.is_empty());
416 }
417
418 #[test]
419 fn clear_multiple_times() {
420 let mut registry = LinkRegistry::new();
421 registry.register("https://a.com");
422 registry.clear();
423 assert!(registry.is_empty());
424 registry.clear(); assert!(registry.is_empty());
426
427 let id = registry.register("https://b.com");
429 assert_ne!(id, 0);
430 assert_eq!(registry.get(id), Some("https://b.com"));
431 }
432
433 #[test]
434 fn ids_sequential_when_no_free_list() {
435 let mut registry = LinkRegistry::new();
436 let id1 = registry.register("https://a.com");
437 let id2 = registry.register("https://b.com");
438 let id3 = registry.register("https://c.com");
439 assert_eq!(id1, 1);
440 assert_eq!(id2, 2);
441 assert_eq!(id3, 3);
442 }
443
444 #[test]
445 fn free_list_mixed_with_fresh_allocation() {
446 let mut registry = LinkRegistry::new();
447 let id1 = registry.register("https://a.com");
448 let id2 = registry.register("https://b.com");
449 let id3 = registry.register("https://c.com");
450
451 registry.unregister(id2);
453
454 let id4 = registry.register("https://d.com");
456 assert_eq!(id4, id2);
457
458 let id5 = registry.register("https://e.com");
460 assert_eq!(id5, 4); assert_eq!(registry.len(), 4); assert_eq!(registry.get(id1), Some("https://a.com"));
465 assert_eq!(registry.get(id4), Some("https://d.com"));
466 assert_eq!(registry.get(id3), Some("https://c.com"));
467 assert_eq!(registry.get(id5), Some("https://e.com"));
468 }
469
470 #[test]
471 fn unregister_does_not_affect_others() {
472 let mut registry = LinkRegistry::new();
473 let id1 = registry.register("https://a.com");
474 let id2 = registry.register("https://b.com");
475 let id3 = registry.register("https://c.com");
476
477 registry.unregister(id2);
478
479 assert_eq!(registry.get(id1), Some("https://a.com"));
480 assert_eq!(registry.get(id2), None);
481 assert_eq!(registry.get(id3), Some("https://c.com"));
482 assert_eq!(registry.len(), 2);
483 }
484
485 #[test]
486 fn dedup_still_works_after_cycle() {
487 let mut registry = LinkRegistry::new();
488 let id1 = registry.register("https://a.com");
489 registry.unregister(id1);
490 let id2 = registry.register("https://a.com");
491 assert_eq!(id1, id2);
493 let id3 = registry.register("https://a.com");
495 assert_eq!(id2, id3);
496 assert_eq!(registry.len(), 1);
497 }
498
499 #[test]
500 fn register_all_freed_then_register_new() {
501 let mut registry = LinkRegistry::new();
502 let ids: Vec<u32> = (0..5)
503 .map(|i| registry.register(&format!("https://u{i}.com")))
504 .collect();
505
506 for &id in &ids {
508 registry.unregister(id);
509 }
510 assert!(registry.is_empty());
511
512 let new_ids: Vec<u32> = (0..5)
514 .map(|i| registry.register(&format!("https://new{i}.com")))
515 .collect();
516 assert_eq!(registry.len(), 5);
517
518 for &new_id in &new_ids {
520 assert!(ids.contains(&new_id));
521 }
522 }
523
524 #[test]
525 fn get_returns_none_after_clear() {
526 let mut registry = LinkRegistry::new();
527 let id = registry.register("https://example.com");
528 registry.clear();
529 assert_eq!(registry.get(id), None);
530 }
531
532 #[test]
533 fn contains_zero_always_false() {
534 let mut registry = LinkRegistry::new();
535 assert!(!registry.contains(0));
536 registry.register("https://example.com");
537 assert!(!registry.contains(0));
538 }
539
540 #[test]
541 fn clone_preserves_free_list() {
542 let mut registry = LinkRegistry::new();
543 let id1 = registry.register("https://a.com");
544 let _id2 = registry.register("https://b.com");
545 registry.unregister(id1);
546
547 let mut cloned = registry.clone();
548 let id3 = cloned.register("https://c.com");
550 assert_eq!(id3, id1); }
552
553 mod property {
554 use super::*;
555 use proptest::prelude::*;
556
557 fn arb_url() -> impl Strategy<Value = String> {
558 "[a-z]{3,12}".prop_map(|s| format!("https://{s}.com"))
559 }
560
561 proptest! {
562 #![proptest_config(ProptestConfig::with_cases(256))]
563
564 #[test]
566 fn register_get_roundtrip(url in arb_url()) {
567 let mut registry = LinkRegistry::new();
568 let id = registry.register(&url);
569 prop_assert_ne!(id, 0);
570 prop_assert_eq!(registry.get(id), Some(url.as_str()));
571 }
572
573 #[test]
575 fn dedup_same_id(url in arb_url()) {
576 let mut registry = LinkRegistry::new();
577 let id1 = registry.register(&url);
578 let id2 = registry.register(&url);
579 prop_assert_eq!(id1, id2);
580 prop_assert_eq!(registry.len(), 1);
581 }
582
583 #[test]
585 fn distinct_urls_distinct_ids(count in 2usize..20) {
586 let mut registry = LinkRegistry::new();
587 let mut ids = Vec::new();
588 for i in 0..count {
589 ids.push(registry.register(&format!("https://u{i}.com")));
590 }
591 for i in 0..ids.len() {
592 for j in (i + 1)..ids.len() {
593 prop_assert_ne!(ids[i], ids[j]);
594 }
595 }
596 }
597
598 #[test]
600 fn len_invariant(n_register in 1usize..15, n_unregister in 0usize..15) {
601 let mut registry = LinkRegistry::new();
602 let mut ids = Vec::new();
603 for i in 0..n_register {
604 ids.push(registry.register(&format!("https://r{i}.com")));
605 }
606 prop_assert_eq!(registry.len(), n_register);
607
608 let actual_unreg = n_unregister.min(n_register);
609 for id in &ids[..actual_unreg] {
610 registry.unregister(*id);
611 }
612 prop_assert_eq!(registry.len(), n_register - actual_unreg);
613 }
614
615 #[test]
617 fn slot_reuse(url1 in arb_url(), url2 in arb_url()) {
618 let mut registry = LinkRegistry::new();
619 let id1 = registry.register(&url1);
620 registry.unregister(id1);
621 let id2 = registry.register(&url2);
622 prop_assert_eq!(id1, id2);
623 prop_assert_eq!(registry.get(id2), Some(url2.as_str()));
624 }
625
626 #[test]
628 fn clear_resets(count in 1usize..15) {
629 let mut registry = LinkRegistry::new();
630 let mut ids = Vec::new();
631 for i in 0..count {
632 ids.push(registry.register(&format!("https://c{i}.com")));
633 }
634 registry.clear();
635 prop_assert!(registry.is_empty());
636 for id in &ids {
637 prop_assert_eq!(registry.get(*id), None);
638 }
639 }
640 }
641 }
642}