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
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn register_and_get() {
142 let mut registry = LinkRegistry::new();
143 let id = registry.register("https://example.com");
144 assert_eq!(registry.get(id), Some("https://example.com"));
145 }
146
147 #[test]
148 fn deduplication() {
149 let mut registry = LinkRegistry::new();
150 let id1 = registry.register("https://example.com");
151 let id2 = registry.register("https://example.com");
152 assert_eq!(id1, id2);
153 assert_eq!(registry.len(), 1);
154 }
155
156 #[test]
157 fn multiple_urls() {
158 let mut registry = LinkRegistry::new();
159 let id1 = registry.register("https://one.com");
160 let id2 = registry.register("https://two.com");
161 assert_ne!(id1, id2);
162 assert_eq!(registry.get(id1), Some("https://one.com"));
163 assert_eq!(registry.get(id2), Some("https://two.com"));
164 }
165
166 #[test]
167 fn unregister_reuses_id() {
168 let mut registry = LinkRegistry::new();
169 let id = registry.register("https://example.com");
170 assert!(registry.contains(id));
171 registry.unregister(id);
172 assert!(!registry.contains(id));
173 let reused = registry.register("https://new.com");
174 assert_eq!(reused, id);
175 }
176
177 #[test]
178 fn clear() {
179 let mut registry = LinkRegistry::new();
180 registry.register("https://one.com");
181 registry.register("https://two.com");
182 assert_eq!(registry.len(), 2);
183 registry.clear();
184 assert!(registry.is_empty());
185 }
186
187 #[test]
190 fn id_zero_is_reserved() {
191 let registry = LinkRegistry::new();
192 assert_eq!(registry.get(0), None);
193 }
194
195 #[test]
196 fn unregister_zero_is_noop() {
197 let mut registry = LinkRegistry::new();
198 registry.register("https://example.com");
199 registry.unregister(0);
200 assert_eq!(registry.len(), 1);
201 }
202
203 #[test]
204 fn get_out_of_bounds_returns_none() {
205 let registry = LinkRegistry::new();
206 assert_eq!(registry.get(999), None);
207 assert_eq!(registry.get(u32::MAX), None);
208 }
209
210 #[test]
211 fn unregister_out_of_bounds_is_safe() {
212 let mut registry = LinkRegistry::new();
213 registry.unregister(999);
214 registry.unregister(u32::MAX);
215 assert!(registry.is_empty());
217 }
218
219 #[test]
220 fn unregister_twice_is_safe() {
221 let mut registry = LinkRegistry::new();
222 let id = registry.register("https://example.com");
223 registry.unregister(id);
224 registry.unregister(id); assert!(registry.is_empty());
226 }
227
228 #[test]
229 fn register_returns_nonzero() {
230 let mut registry = LinkRegistry::new();
231 for i in 0..20 {
232 let id = registry.register(&format!("https://example.com/{i}"));
233 assert_ne!(id, 0, "register must never return id 0");
234 }
235 }
236
237 #[test]
238 fn contains_after_unregister() {
239 let mut registry = LinkRegistry::new();
240 let id = registry.register("https://example.com");
241 assert!(registry.contains(id));
242 registry.unregister(id);
243 assert!(!registry.contains(id));
244 }
245
246 #[test]
247 fn contains_invalid_id() {
248 let registry = LinkRegistry::new();
249 assert!(!registry.contains(0));
250 assert!(!registry.contains(999));
251 }
252
253 #[test]
254 fn dedup_after_unregister_gets_new_id() {
255 let mut registry = LinkRegistry::new();
256 let id1 = registry.register("https://example.com");
257 registry.unregister(id1);
258 let id2 = registry.register("https://example.com");
260 assert_eq!(id2, id1); assert_eq!(registry.get(id2), Some("https://example.com"));
262 assert_eq!(registry.len(), 1);
263 }
264
265 #[test]
266 fn free_list_lifo_order() {
267 let mut registry = LinkRegistry::new();
268 let a = registry.register("https://a.com");
269 let b = registry.register("https://b.com");
270 let c = registry.register("https://c.com");
271
272 registry.unregister(a);
274 registry.unregister(b);
275 registry.unregister(c);
276
277 let new1 = registry.register("https://new1.com");
279 assert_eq!(new1, c);
280 let new2 = registry.register("https://new2.com");
281 assert_eq!(new2, b);
282 let new3 = registry.register("https://new3.com");
283 assert_eq!(new3, a);
284 }
285
286 #[test]
287 fn len_tracks_operations() {
288 let mut registry = LinkRegistry::new();
289 assert_eq!(registry.len(), 0);
290
291 let id1 = registry.register("https://one.com");
292 assert_eq!(registry.len(), 1);
293
294 let id2 = registry.register("https://two.com");
295 assert_eq!(registry.len(), 2);
296
297 registry.register("https://one.com");
299 assert_eq!(registry.len(), 2);
300
301 registry.unregister(id1);
302 assert_eq!(registry.len(), 1);
303
304 registry.unregister(id2);
305 assert_eq!(registry.len(), 0);
306 assert!(registry.is_empty());
307 }
308
309 #[test]
310 fn register_after_clear_works() {
311 let mut registry = LinkRegistry::new();
312 registry.register("https://one.com");
313 registry.register("https://two.com");
314 registry.clear();
315
316 let id = registry.register("https://fresh.com");
317 assert_ne!(id, 0);
318 assert_eq!(registry.get(id), Some("https://fresh.com"));
319 assert_eq!(registry.len(), 1);
320 }
321
322 #[test]
323 fn many_registrations() {
324 let mut registry = LinkRegistry::new();
325 let mut ids = Vec::new();
326 for i in 0..100 {
327 let url = format!("https://example.com/{i}");
328 ids.push(registry.register(&url));
329 }
330 assert_eq!(registry.len(), 100);
331
332 for (i, &id) in ids.iter().enumerate() {
334 assert_ne!(id, 0);
335 let url = format!("https://example.com/{i}");
336 assert_eq!(registry.get(id), Some(url.as_str()));
337 }
338
339 let mut sorted = ids.clone();
341 sorted.sort();
342 sorted.dedup();
343 assert_eq!(sorted.len(), ids.len());
344 }
345
346 #[test]
351 fn default_trait_creates_empty_registry() {
352 let registry = LinkRegistry::default();
353 assert!(registry.is_empty());
354 assert_eq!(registry.len(), 0);
355 assert_eq!(registry.get(0), None);
356 }
357
358 #[test]
359 fn clone_independence() {
360 let mut original = LinkRegistry::new();
361 let id = original.register("https://example.com");
362 let mut cloned = original.clone();
363
364 cloned.unregister(id);
366 assert!(!cloned.contains(id));
367
368 assert!(original.contains(id));
370 assert_eq!(original.get(id), Some("https://example.com"));
371 }
372
373 #[test]
374 fn debug_formatting() {
375 let mut registry = LinkRegistry::new();
376 registry.register("https://example.com");
377 let dbg = format!("{:?}", registry);
378 assert!(dbg.contains("LinkRegistry"));
379 assert!(dbg.contains("example.com"));
380 }
381
382 #[test]
383 fn register_empty_url() {
384 let mut registry = LinkRegistry::new();
385 let id = registry.register("");
386 assert_ne!(id, 0);
387 assert_eq!(registry.get(id), Some(""));
388 assert_eq!(registry.len(), 1);
389 }
390
391 #[test]
392 fn register_url_with_special_chars() {
393 let mut registry = LinkRegistry::new();
394 let url = "https://example.com/path?q=hello world&foo=bar#section";
395 let id = registry.register(url);
396 assert_eq!(registry.get(id), Some(url));
397 }
398
399 #[test]
400 fn register_url_with_unicode() {
401 let mut registry = LinkRegistry::new();
402 let url = "https://例え.jp/日本語";
403 let id = registry.register(url);
404 assert_eq!(registry.get(id), Some(url));
405 }
406
407 #[test]
408 fn register_very_long_url() {
409 let mut registry = LinkRegistry::new();
410 let url = format!("https://example.com/{}", "a".repeat(10_000));
411 let id = registry.register(&url);
412 assert_eq!(registry.get(id), Some(url.as_str()));
413 }
414
415 #[test]
416 fn is_empty_on_fresh_registry() {
417 let registry = LinkRegistry::new();
418 assert!(registry.is_empty());
419 assert_eq!(registry.len(), 0);
420 }
421
422 #[test]
423 fn is_empty_after_register_unregister() {
424 let mut registry = LinkRegistry::new();
425 let id = registry.register("https://test.com");
426 assert!(!registry.is_empty());
427 registry.unregister(id);
428 assert!(registry.is_empty());
429 }
430
431 #[test]
432 fn clear_multiple_times() {
433 let mut registry = LinkRegistry::new();
434 registry.register("https://a.com");
435 registry.clear();
436 assert!(registry.is_empty());
437 registry.clear(); assert!(registry.is_empty());
439
440 let id = registry.register("https://b.com");
442 assert_ne!(id, 0);
443 assert_eq!(registry.get(id), Some("https://b.com"));
444 }
445
446 #[test]
447 fn ids_sequential_when_no_free_list() {
448 let mut registry = LinkRegistry::new();
449 let id1 = registry.register("https://a.com");
450 let id2 = registry.register("https://b.com");
451 let id3 = registry.register("https://c.com");
452 assert_eq!(id1, 1);
453 assert_eq!(id2, 2);
454 assert_eq!(id3, 3);
455 }
456
457 #[test]
458 fn free_list_mixed_with_fresh_allocation() {
459 let mut registry = LinkRegistry::new();
460 let id1 = registry.register("https://a.com");
461 let id2 = registry.register("https://b.com");
462 let id3 = registry.register("https://c.com");
463
464 registry.unregister(id2);
466
467 let id4 = registry.register("https://d.com");
469 assert_eq!(id4, id2);
470
471 let id5 = registry.register("https://e.com");
473 assert_eq!(id5, 4); assert_eq!(registry.len(), 4); assert_eq!(registry.get(id1), Some("https://a.com"));
478 assert_eq!(registry.get(id4), Some("https://d.com"));
479 assert_eq!(registry.get(id3), Some("https://c.com"));
480 assert_eq!(registry.get(id5), Some("https://e.com"));
481 }
482
483 #[test]
484 fn unregister_does_not_affect_others() {
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
490 registry.unregister(id2);
491
492 assert_eq!(registry.get(id1), Some("https://a.com"));
493 assert_eq!(registry.get(id2), None);
494 assert_eq!(registry.get(id3), Some("https://c.com"));
495 assert_eq!(registry.len(), 2);
496 }
497
498 #[test]
499 fn dedup_still_works_after_cycle() {
500 let mut registry = LinkRegistry::new();
501 let id1 = registry.register("https://a.com");
502 registry.unregister(id1);
503 let id2 = registry.register("https://a.com");
504 assert_eq!(id1, id2);
506 let id3 = registry.register("https://a.com");
508 assert_eq!(id2, id3);
509 assert_eq!(registry.len(), 1);
510 }
511
512 #[test]
513 fn register_all_freed_then_register_new() {
514 let mut registry = LinkRegistry::new();
515 let ids: Vec<u32> = (0..5)
516 .map(|i| registry.register(&format!("https://u{i}.com")))
517 .collect();
518
519 for &id in &ids {
521 registry.unregister(id);
522 }
523 assert!(registry.is_empty());
524
525 let new_ids: Vec<u32> = (0..5)
527 .map(|i| registry.register(&format!("https://new{i}.com")))
528 .collect();
529 assert_eq!(registry.len(), 5);
530
531 for &new_id in &new_ids {
533 assert!(ids.contains(&new_id));
534 }
535 }
536
537 #[test]
538 fn get_returns_none_after_clear() {
539 let mut registry = LinkRegistry::new();
540 let id = registry.register("https://example.com");
541 registry.clear();
542 assert_eq!(registry.get(id), None);
543 }
544
545 #[test]
546 fn contains_zero_always_false() {
547 let mut registry = LinkRegistry::new();
548 assert!(!registry.contains(0));
549 registry.register("https://example.com");
550 assert!(!registry.contains(0));
551 }
552
553 #[test]
554 fn clone_preserves_free_list() {
555 let mut registry = LinkRegistry::new();
556 let id1 = registry.register("https://a.com");
557 let _id2 = registry.register("https://b.com");
558 registry.unregister(id1);
559
560 let mut cloned = registry.clone();
561 let id3 = cloned.register("https://c.com");
563 assert_eq!(id3, id1); }
565
566 #[test]
567 fn register_rejects_control_chars() {
568 let mut registry = LinkRegistry::new();
569 let id = registry.register("https://example.com/\x1b]52;c;boom");
570 assert_eq!(id, 0);
571 assert!(registry.is_empty());
572 }
573
574 #[test]
575 fn register_rejects_overlong_urls() {
576 let mut registry = LinkRegistry::new();
577 let overlong = format!("https://example.com/{}", "a".repeat(MAX_URL_BYTES));
578 let id = registry.register(&overlong);
579 assert_eq!(id, 0);
580 assert!(registry.is_empty());
581 }
582
583 mod property {
584 use super::*;
585 use proptest::prelude::*;
586
587 fn arb_url() -> impl Strategy<Value = String> {
588 "[a-z]{3,12}".prop_map(|s| format!("https://{s}.com"))
589 }
590
591 proptest! {
592 #![proptest_config(ProptestConfig::with_cases(256))]
593
594 #[test]
596 fn register_get_roundtrip(url in arb_url()) {
597 let mut registry = LinkRegistry::new();
598 let id = registry.register(&url);
599 prop_assert_ne!(id, 0);
600 prop_assert_eq!(registry.get(id), Some(url.as_str()));
601 }
602
603 #[test]
605 fn dedup_same_id(url in arb_url()) {
606 let mut registry = LinkRegistry::new();
607 let id1 = registry.register(&url);
608 let id2 = registry.register(&url);
609 prop_assert_eq!(id1, id2);
610 prop_assert_eq!(registry.len(), 1);
611 }
612
613 #[test]
615 fn distinct_urls_distinct_ids(count in 2usize..20) {
616 let mut registry = LinkRegistry::new();
617 let mut ids = Vec::new();
618 for i in 0..count {
619 ids.push(registry.register(&format!("https://u{i}.com")));
620 }
621 for i in 0..ids.len() {
622 for j in (i + 1)..ids.len() {
623 prop_assert_ne!(ids[i], ids[j]);
624 }
625 }
626 }
627
628 #[test]
630 fn len_invariant(n_register in 1usize..15, n_unregister in 0usize..15) {
631 let mut registry = LinkRegistry::new();
632 let mut ids = Vec::new();
633 for i in 0..n_register {
634 ids.push(registry.register(&format!("https://r{i}.com")));
635 }
636 prop_assert_eq!(registry.len(), n_register);
637
638 let actual_unreg = n_unregister.min(n_register);
639 for id in &ids[..actual_unreg] {
640 registry.unregister(*id);
641 }
642 prop_assert_eq!(registry.len(), n_register - actual_unreg);
643 }
644
645 #[test]
647 fn slot_reuse(url1 in arb_url(), url2 in arb_url()) {
648 let mut registry = LinkRegistry::new();
649 let id1 = registry.register(&url1);
650 registry.unregister(id1);
651 let id2 = registry.register(&url2);
652 prop_assert_eq!(id1, id2);
653 prop_assert_eq!(registry.get(id2), Some(url2.as_str()));
654 }
655
656 #[test]
658 fn clear_resets(count in 1usize..15) {
659 let mut registry = LinkRegistry::new();
660 let mut ids = Vec::new();
661 for i in 0..count {
662 ids.push(registry.register(&format!("https://c{i}.com")));
663 }
664 registry.clear();
665 prop_assert!(registry.is_empty());
666 for id in &ids {
667 prop_assert_eq!(registry.get(*id), None);
668 }
669 }
670 }
671 }
672}