1#![forbid(unsafe_code)]
2
3use ahash::AHashMap;
19
20const MAX_LINK_ID: u32 = 0x00FF_FFFF;
21const MAX_URL_BYTES: usize = 4096;
22
23#[inline]
24fn is_safe_osc8_url(url: &str) -> bool {
25 if url.len() > MAX_URL_BYTES {
26 return false;
27 }
28 !url.chars().any(char::is_control)
29}
30
31#[derive(Debug, Clone, Default)]
33pub struct LinkRegistry {
34 links: Vec<Option<String>>,
36 lookup: AHashMap<String, u32>,
38 free_list: Vec<u32>,
40}
41
42impl LinkRegistry {
43 pub fn new() -> Self {
45 Self {
46 links: vec![None],
47 lookup: AHashMap::new(),
48 free_list: Vec::new(),
49 }
50 }
51
52 pub fn register(&mut self, url: &str) -> u32 {
56 if !is_safe_osc8_url(url) {
57 return 0;
58 }
59
60 if let Some(&id) = self.lookup.get(url) {
61 return id;
62 }
63
64 let id = if let Some(id) = self.free_list.pop() {
65 id
66 } else {
67 let id = self.links.len() as u32;
68 debug_assert!(id <= MAX_LINK_ID, "link id overflow");
69 if id > MAX_LINK_ID {
70 return 0;
71 }
72 self.links.push(None);
73 id
74 };
75
76 if id == 0 || id > MAX_LINK_ID {
77 return 0;
78 }
79
80 self.links[id as usize] = Some(url.to_string());
81 self.lookup.insert(url.to_string(), id);
82 id
83 }
84
85 #[must_use]
87 pub fn get(&self, id: u32) -> Option<&str> {
88 self.links
89 .get(id as usize)
90 .and_then(|slot| slot.as_ref())
91 .map(|s| s.as_str())
92 }
93
94 pub fn unregister(&mut self, id: u32) {
96 if id == 0 {
97 return;
98 }
99
100 let Some(slot) = self.links.get_mut(id as usize) else {
101 return;
102 };
103
104 if let Some(url) = slot.take() {
105 self.lookup.remove(&url);
106 self.free_list.push(id);
107 }
108 }
109
110 pub fn clear(&mut self) {
112 self.links.clear();
113 self.links.push(None);
114 self.lookup.clear();
115 self.free_list.clear();
116 }
117
118 pub fn len(&self) -> usize {
120 self.links.iter().filter(|slot| slot.is_some()).count()
121 }
122
123 #[inline]
125 pub fn is_empty(&self) -> bool {
126 self.len() == 0
127 }
128
129 #[inline]
131 pub fn contains(&self, id: u32) -> bool {
132 self.get(id).is_some()
133 }
134
135 #[must_use]
139 pub fn estimate_memory(&self) -> usize {
140 let mut total = 0usize;
141
142 total += self.links.capacity() * core::mem::size_of::<Option<String>>();
143 total += self
144 .links
145 .iter()
146 .flatten()
147 .map(String::capacity)
148 .sum::<usize>();
149
150 total += self.lookup.capacity() * core::mem::size_of::<(String, u32)>();
151 total += self.lookup.keys().map(String::capacity).sum::<usize>();
152
153 total += self.free_list.capacity() * core::mem::size_of::<u32>();
154
155 total
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn register_and_get() {
165 let mut registry = LinkRegistry::new();
166 let id = registry.register("https://example.com");
167 assert_eq!(registry.get(id), Some("https://example.com"));
168 }
169
170 #[test]
171 fn deduplication() {
172 let mut registry = LinkRegistry::new();
173 let id1 = registry.register("https://example.com");
174 let id2 = registry.register("https://example.com");
175 assert_eq!(id1, id2);
176 assert_eq!(registry.len(), 1);
177 }
178
179 #[test]
180 fn multiple_urls() {
181 let mut registry = LinkRegistry::new();
182 let id1 = registry.register("https://one.com");
183 let id2 = registry.register("https://two.com");
184 assert_ne!(id1, id2);
185 assert_eq!(registry.get(id1), Some("https://one.com"));
186 assert_eq!(registry.get(id2), Some("https://two.com"));
187 }
188
189 #[test]
190 fn unregister_reuses_id() {
191 let mut registry = LinkRegistry::new();
192 let id = registry.register("https://example.com");
193 assert!(registry.contains(id));
194 registry.unregister(id);
195 assert!(!registry.contains(id));
196 let reused = registry.register("https://new.com");
197 assert_eq!(reused, id);
198 }
199
200 #[test]
201 fn clear() {
202 let mut registry = LinkRegistry::new();
203 registry.register("https://one.com");
204 registry.register("https://two.com");
205 assert_eq!(registry.len(), 2);
206 registry.clear();
207 assert!(registry.is_empty());
208 }
209
210 #[test]
213 fn id_zero_is_reserved() {
214 let registry = LinkRegistry::new();
215 assert_eq!(registry.get(0), None);
216 }
217
218 #[test]
219 fn unregister_zero_is_noop() {
220 let mut registry = LinkRegistry::new();
221 registry.register("https://example.com");
222 registry.unregister(0);
223 assert_eq!(registry.len(), 1);
224 }
225
226 #[test]
227 fn get_out_of_bounds_returns_none() {
228 let registry = LinkRegistry::new();
229 assert_eq!(registry.get(999), None);
230 assert_eq!(registry.get(u32::MAX), None);
231 }
232
233 #[test]
234 fn unregister_out_of_bounds_is_safe() {
235 let mut registry = LinkRegistry::new();
236 registry.unregister(999);
237 registry.unregister(u32::MAX);
238 assert!(registry.is_empty());
240 }
241
242 #[test]
243 fn unregister_twice_is_safe() {
244 let mut registry = LinkRegistry::new();
245 let id = registry.register("https://example.com");
246 registry.unregister(id);
247 registry.unregister(id); assert!(registry.is_empty());
249 }
250
251 #[test]
252 fn register_returns_nonzero() {
253 let mut registry = LinkRegistry::new();
254 for i in 0..20 {
255 let id = registry.register(&format!("https://example.com/{i}"));
256 assert_ne!(id, 0, "register must never return id 0");
257 }
258 }
259
260 #[test]
261 fn contains_after_unregister() {
262 let mut registry = LinkRegistry::new();
263 let id = registry.register("https://example.com");
264 assert!(registry.contains(id));
265 registry.unregister(id);
266 assert!(!registry.contains(id));
267 }
268
269 #[test]
270 fn contains_invalid_id() {
271 let registry = LinkRegistry::new();
272 assert!(!registry.contains(0));
273 assert!(!registry.contains(999));
274 }
275
276 #[test]
277 fn dedup_after_unregister_gets_new_id() {
278 let mut registry = LinkRegistry::new();
279 let id1 = registry.register("https://example.com");
280 registry.unregister(id1);
281 let id2 = registry.register("https://example.com");
283 assert_eq!(id2, id1); assert_eq!(registry.get(id2), Some("https://example.com"));
285 assert_eq!(registry.len(), 1);
286 }
287
288 #[test]
289 fn free_list_lifo_order() {
290 let mut registry = LinkRegistry::new();
291 let a = registry.register("https://a.com");
292 let b = registry.register("https://b.com");
293 let c = registry.register("https://c.com");
294
295 registry.unregister(a);
297 registry.unregister(b);
298 registry.unregister(c);
299
300 let new1 = registry.register("https://new1.com");
302 assert_eq!(new1, c);
303 let new2 = registry.register("https://new2.com");
304 assert_eq!(new2, b);
305 let new3 = registry.register("https://new3.com");
306 assert_eq!(new3, a);
307 }
308
309 #[test]
310 fn len_tracks_operations() {
311 let mut registry = LinkRegistry::new();
312 assert_eq!(registry.len(), 0);
313
314 let id1 = registry.register("https://one.com");
315 assert_eq!(registry.len(), 1);
316
317 let id2 = registry.register("https://two.com");
318 assert_eq!(registry.len(), 2);
319
320 registry.register("https://one.com");
322 assert_eq!(registry.len(), 2);
323
324 registry.unregister(id1);
325 assert_eq!(registry.len(), 1);
326
327 registry.unregister(id2);
328 assert_eq!(registry.len(), 0);
329 assert!(registry.is_empty());
330 }
331
332 #[test]
333 fn register_after_clear_works() {
334 let mut registry = LinkRegistry::new();
335 registry.register("https://one.com");
336 registry.register("https://two.com");
337 registry.clear();
338
339 let id = registry.register("https://fresh.com");
340 assert_ne!(id, 0);
341 assert_eq!(registry.get(id), Some("https://fresh.com"));
342 assert_eq!(registry.len(), 1);
343 }
344
345 #[test]
346 fn many_registrations() {
347 let mut registry = LinkRegistry::new();
348 let mut ids = Vec::new();
349 for i in 0..100 {
350 let url = format!("https://example.com/{i}");
351 ids.push(registry.register(&url));
352 }
353 assert_eq!(registry.len(), 100);
354
355 for (i, &id) in ids.iter().enumerate() {
357 assert_ne!(id, 0);
358 let url = format!("https://example.com/{i}");
359 assert_eq!(registry.get(id), Some(url.as_str()));
360 }
361
362 let mut sorted = ids.clone();
364 sorted.sort();
365 sorted.dedup();
366 assert_eq!(sorted.len(), ids.len());
367 }
368
369 #[test]
374 fn default_trait_creates_empty_registry() {
375 let registry = LinkRegistry::default();
376 assert!(registry.is_empty());
377 assert_eq!(registry.len(), 0);
378 assert_eq!(registry.get(0), None);
379 }
380
381 #[test]
382 fn clone_independence() {
383 let mut original = LinkRegistry::new();
384 let id = original.register("https://example.com");
385 let mut cloned = original.clone();
386
387 cloned.unregister(id);
389 assert!(!cloned.contains(id));
390
391 assert!(original.contains(id));
393 assert_eq!(original.get(id), Some("https://example.com"));
394 }
395
396 #[test]
397 fn debug_formatting() {
398 let mut registry = LinkRegistry::new();
399 registry.register("https://example.com");
400 let dbg = format!("{:?}", registry);
401 assert!(dbg.contains("LinkRegistry"));
402 assert!(dbg.contains("example.com"));
403 }
404
405 #[test]
406 fn register_empty_url() {
407 let mut registry = LinkRegistry::new();
408 let id = registry.register("");
409 assert_ne!(id, 0);
410 assert_eq!(registry.get(id), Some(""));
411 assert_eq!(registry.len(), 1);
412 }
413
414 #[test]
415 fn register_url_with_special_chars() {
416 let mut registry = LinkRegistry::new();
417 let url = "https://example.com/path?q=hello world&foo=bar#section";
418 let id = registry.register(url);
419 assert_eq!(registry.get(id), Some(url));
420 }
421
422 #[test]
423 fn register_url_with_unicode() {
424 let mut registry = LinkRegistry::new();
425 let url = "https://例え.jp/日本語";
426 let id = registry.register(url);
427 assert_eq!(registry.get(id), Some(url));
428 }
429
430 #[test]
431 fn register_very_long_url() {
432 let mut registry = LinkRegistry::new();
433 let url = format!("https://example.com/{}", "a".repeat(10_000));
434 let id = registry.register(&url);
435 assert_eq!(id, 0);
436 assert_eq!(registry.get(id), None);
437 }
438
439 #[test]
440 fn register_max_length_url() {
441 let mut registry = LinkRegistry::new();
442 let prefix = "https://example.com/";
443 let suffix_len = MAX_URL_BYTES.saturating_sub(prefix.len());
444 let url = format!("{prefix}{}", "a".repeat(suffix_len));
445 assert_eq!(url.len(), MAX_URL_BYTES);
446
447 let id = registry.register(&url);
448 assert_ne!(id, 0);
449 assert_eq!(registry.get(id), Some(url.as_str()));
450 }
451
452 #[test]
453 fn is_empty_on_fresh_registry() {
454 let registry = LinkRegistry::new();
455 assert!(registry.is_empty());
456 assert_eq!(registry.len(), 0);
457 }
458
459 #[test]
460 fn is_empty_after_register_unregister() {
461 let mut registry = LinkRegistry::new();
462 let id = registry.register("https://test.com");
463 assert!(!registry.is_empty());
464 registry.unregister(id);
465 assert!(registry.is_empty());
466 }
467
468 #[test]
469 fn clear_multiple_times() {
470 let mut registry = LinkRegistry::new();
471 registry.register("https://a.com");
472 registry.clear();
473 assert!(registry.is_empty());
474 registry.clear(); assert!(registry.is_empty());
476
477 let id = registry.register("https://b.com");
479 assert_ne!(id, 0);
480 assert_eq!(registry.get(id), Some("https://b.com"));
481 }
482
483 #[test]
484 fn ids_sequential_when_no_free_list() {
485 let mut registry = LinkRegistry::new();
486 let id1 = registry.register("https://a.com");
487 let id2 = registry.register("https://b.com");
488 let id3 = registry.register("https://c.com");
489 assert_eq!(id1, 1);
490 assert_eq!(id2, 2);
491 assert_eq!(id3, 3);
492 }
493
494 #[test]
495 fn free_list_mixed_with_fresh_allocation() {
496 let mut registry = LinkRegistry::new();
497 let id1 = registry.register("https://a.com");
498 let id2 = registry.register("https://b.com");
499 let id3 = registry.register("https://c.com");
500
501 registry.unregister(id2);
503
504 let id4 = registry.register("https://d.com");
506 assert_eq!(id4, id2);
507
508 let id5 = registry.register("https://e.com");
510 assert_eq!(id5, 4); assert_eq!(registry.len(), 4); assert_eq!(registry.get(id1), Some("https://a.com"));
515 assert_eq!(registry.get(id4), Some("https://d.com"));
516 assert_eq!(registry.get(id3), Some("https://c.com"));
517 assert_eq!(registry.get(id5), Some("https://e.com"));
518 }
519
520 #[test]
521 fn unregister_does_not_affect_others() {
522 let mut registry = LinkRegistry::new();
523 let id1 = registry.register("https://a.com");
524 let id2 = registry.register("https://b.com");
525 let id3 = registry.register("https://c.com");
526
527 registry.unregister(id2);
528
529 assert_eq!(registry.get(id1), Some("https://a.com"));
530 assert_eq!(registry.get(id2), None);
531 assert_eq!(registry.get(id3), Some("https://c.com"));
532 assert_eq!(registry.len(), 2);
533 }
534
535 #[test]
536 fn dedup_still_works_after_cycle() {
537 let mut registry = LinkRegistry::new();
538 let id1 = registry.register("https://a.com");
539 registry.unregister(id1);
540 let id2 = registry.register("https://a.com");
541 assert_eq!(id1, id2);
543 let id3 = registry.register("https://a.com");
545 assert_eq!(id2, id3);
546 assert_eq!(registry.len(), 1);
547 }
548
549 #[test]
550 fn register_all_freed_then_register_new() {
551 let mut registry = LinkRegistry::new();
552 let ids: Vec<u32> = (0..5)
553 .map(|i| registry.register(&format!("https://u{i}.com")))
554 .collect();
555
556 for &id in &ids {
558 registry.unregister(id);
559 }
560 assert!(registry.is_empty());
561
562 let new_ids: Vec<u32> = (0..5)
564 .map(|i| registry.register(&format!("https://new{i}.com")))
565 .collect();
566 assert_eq!(registry.len(), 5);
567
568 for &new_id in &new_ids {
570 assert!(ids.contains(&new_id));
571 }
572 }
573
574 #[test]
575 fn get_returns_none_after_clear() {
576 let mut registry = LinkRegistry::new();
577 let id = registry.register("https://example.com");
578 registry.clear();
579 assert_eq!(registry.get(id), None);
580 }
581
582 #[test]
583 fn contains_zero_always_false() {
584 let mut registry = LinkRegistry::new();
585 assert!(!registry.contains(0));
586 registry.register("https://example.com");
587 assert!(!registry.contains(0));
588 }
589
590 #[test]
591 fn clone_preserves_free_list() {
592 let mut registry = LinkRegistry::new();
593 let id1 = registry.register("https://a.com");
594 let _id2 = registry.register("https://b.com");
595 registry.unregister(id1);
596
597 let mut cloned = registry.clone();
598 let id3 = cloned.register("https://c.com");
600 assert_eq!(id3, id1); }
602
603 #[test]
604 fn register_rejects_control_chars() {
605 let mut registry = LinkRegistry::new();
606 let id = registry.register("https://example.com/\x1b]52;c;boom");
607 assert_eq!(id, 0);
608 assert!(registry.is_empty());
609 }
610
611 #[test]
612 fn register_rejects_overlong_urls() {
613 let mut registry = LinkRegistry::new();
614 let overlong = format!("https://example.com/{}", "a".repeat(MAX_URL_BYTES));
615 let id = registry.register(&overlong);
616 assert_eq!(id, 0);
617 assert!(registry.is_empty());
618 }
619
620 mod property {
621 use super::*;
622 use proptest::prelude::*;
623
624 fn arb_url() -> impl Strategy<Value = String> {
625 "[a-z]{3,12}".prop_map(|s| format!("https://{s}.com"))
626 }
627
628 proptest! {
629 #![proptest_config(ProptestConfig::with_cases(256))]
630
631 #[test]
633 fn register_get_roundtrip(url in arb_url()) {
634 let mut registry = LinkRegistry::new();
635 let id = registry.register(&url);
636 prop_assert_ne!(id, 0);
637 prop_assert_eq!(registry.get(id), Some(url.as_str()));
638 }
639
640 #[test]
642 fn dedup_same_id(url in arb_url()) {
643 let mut registry = LinkRegistry::new();
644 let id1 = registry.register(&url);
645 let id2 = registry.register(&url);
646 prop_assert_eq!(id1, id2);
647 prop_assert_eq!(registry.len(), 1);
648 }
649
650 #[test]
652 fn distinct_urls_distinct_ids(count in 2usize..20) {
653 let mut registry = LinkRegistry::new();
654 let mut ids = Vec::new();
655 for i in 0..count {
656 ids.push(registry.register(&format!("https://u{i}.com")));
657 }
658 for i in 0..ids.len() {
659 for j in (i + 1)..ids.len() {
660 prop_assert_ne!(ids[i], ids[j]);
661 }
662 }
663 }
664
665 #[test]
667 fn len_invariant(n_register in 1usize..15, n_unregister in 0usize..15) {
668 let mut registry = LinkRegistry::new();
669 let mut ids = Vec::new();
670 for i in 0..n_register {
671 ids.push(registry.register(&format!("https://r{i}.com")));
672 }
673 prop_assert_eq!(registry.len(), n_register);
674
675 let actual_unreg = n_unregister.min(n_register);
676 for id in &ids[..actual_unreg] {
677 registry.unregister(*id);
678 }
679 prop_assert_eq!(registry.len(), n_register - actual_unreg);
680 }
681
682 #[test]
684 fn slot_reuse(url1 in arb_url(), url2 in arb_url()) {
685 let mut registry = LinkRegistry::new();
686 let id1 = registry.register(&url1);
687 registry.unregister(id1);
688 let id2 = registry.register(&url2);
689 prop_assert_eq!(id1, id2);
690 prop_assert_eq!(registry.get(id2), Some(url2.as_str()));
691 }
692
693 #[test]
695 fn clear_resets(count in 1usize..15) {
696 let mut registry = LinkRegistry::new();
697 let mut ids = Vec::new();
698 for i in 0..count {
699 ids.push(registry.register(&format!("https://c{i}.com")));
700 }
701 registry.clear();
702 prop_assert!(registry.is_empty());
703 for id in &ids {
704 prop_assert_eq!(registry.get(*id), None);
705 }
706 }
707 }
708 }
709}