1use crate::content_id::{ContentId, DefaultContentHasher, Hasher};
18use crate::contentable::{Contentable, ContentableError};
19use std::collections::HashMap;
20use std::hash::Hash as StdHash;
21use std::sync::atomic::{AtomicU64, Ordering};
22
23#[derive(Debug, Clone, Default)]
25pub struct CacheMetrics {
26 pub hits: u64,
28 pub misses: u64,
30 pub size: usize,
32}
33
34impl CacheMetrics {
35 #[must_use]
37 #[allow(clippy::as_conversions)] pub fn hit_rate(&self) -> f64 {
39 let total = self.hits + self.misses;
40 if total == 0 {
41 0.0
42 } else {
43 (self.hits as f64 / total as f64) * 100.0
44 }
45 }
46}
47
48#[derive(Debug)]
77pub struct ContentStore<K: Contentable, V, H: Hasher + Eq + StdHash = DefaultContentHasher> {
78 store: HashMap<ContentId<H>, V>,
79 collision_witnesses: Option<HashMap<ContentId<H>, Vec<u8>>>,
80 hits: AtomicU64,
81 misses: AtomicU64,
82 _key: std::marker::PhantomData<K>,
83}
84
85impl<K: Contentable, V, H: Hasher + Eq + StdHash> Default for ContentStore<K, V, H> {
86 fn default() -> Self {
87 Self::new()
88 }
89}
90
91impl<K: Contentable, V, H: Hasher + Eq + StdHash> ContentStore<K, V, H> {
92 #[must_use]
94 pub fn new() -> Self {
95 Self {
96 store: HashMap::new(),
97 collision_witnesses: None,
98 hits: AtomicU64::new(0),
99 misses: AtomicU64::new(0),
100 _key: std::marker::PhantomData,
101 }
102 }
103
104 #[must_use]
107 pub fn new_collision_defended() -> Self {
108 Self {
109 store: HashMap::new(),
110 collision_witnesses: Some(HashMap::new()),
111 hits: AtomicU64::new(0),
112 misses: AtomicU64::new(0),
113 _key: std::marker::PhantomData,
114 }
115 }
116
117 #[must_use]
119 pub fn with_capacity(capacity: usize) -> Self {
120 Self {
121 store: HashMap::with_capacity(capacity),
122 collision_witnesses: None,
123 hits: AtomicU64::new(0),
124 misses: AtomicU64::new(0),
125 _key: std::marker::PhantomData,
126 }
127 }
128
129 #[must_use]
131 pub fn with_capacity_collision_defended(capacity: usize) -> Self {
132 Self {
133 store: HashMap::with_capacity(capacity),
134 collision_witnesses: Some(HashMap::with_capacity(capacity)),
135 hits: AtomicU64::new(0),
136 misses: AtomicU64::new(0),
137 _key: std::marker::PhantomData,
138 }
139 }
140
141 pub fn get(&self, key: &K) -> Result<Option<&V>, ContentableError> {
150 let cid = key.content_id::<H>()?;
151 if let Some(v) = self.store.get(&cid) {
152 if let Some(witnesses) = &self.collision_witnesses {
153 let bytes = key.to_bytes()?;
154 if witnesses.get(&cid).is_some_and(|stored| stored != &bytes) {
155 return Err(ContentableError::InvalidFormat(
156 "content-id collision detected during get".to_string(),
157 ));
158 }
159 }
160 self.hits.fetch_add(1, Ordering::Relaxed);
161 return Ok(Some(v));
162 }
163 self.misses.fetch_add(1, Ordering::Relaxed);
164 Ok(None)
165 }
166
167 pub fn insert(&mut self, key: &K, value: V) -> Result<Option<V>, ContentableError> {
176 let cid = key.content_id::<H>()?;
177 if let Some(witnesses) = &mut self.collision_witnesses {
178 let bytes = key.to_bytes()?;
179 if let Some(stored) = witnesses.get(&cid) {
180 if stored != &bytes {
181 return Err(ContentableError::InvalidFormat(
182 "content-id collision detected during insert".to_string(),
183 ));
184 }
185 } else {
186 witnesses.insert(cid.clone(), bytes);
187 }
188 }
189 Ok(self.store.insert(cid, value))
190 }
191
192 pub fn get_or_insert_with<F>(&mut self, key: &K, f: F) -> Result<&V, ContentableError>
203 where
204 F: FnOnce() -> V,
205 {
206 let cid = key.content_id::<H>()?;
207 if let Some(witnesses) = &mut self.collision_witnesses {
208 let bytes = key.to_bytes()?;
209 if let Some(stored) = witnesses.get(&cid) {
210 if stored != &bytes {
211 return Err(ContentableError::InvalidFormat(
212 "content-id collision detected during get_or_insert_with".to_string(),
213 ));
214 }
215 } else {
216 witnesses.insert(cid.clone(), bytes);
217 }
218 }
219 match self.store.entry(cid) {
220 std::collections::hash_map::Entry::Occupied(entry) => {
221 self.hits.fetch_add(1, Ordering::Relaxed);
222 Ok(entry.into_mut())
223 }
224 std::collections::hash_map::Entry::Vacant(entry) => {
225 self.misses.fetch_add(1, Ordering::Relaxed);
226 Ok(entry.insert(f()))
227 }
228 }
229 }
230
231 pub fn contains(&self, key: &K) -> Result<bool, ContentableError> {
237 let cid = key.content_id::<H>()?;
238 Ok(self.store.contains_key(&cid))
239 }
240
241 pub fn remove(&mut self, key: &K) -> Result<Option<V>, ContentableError> {
247 let cid = key.content_id::<H>()?;
248 let removed = self.store.remove(&cid);
249 if removed.is_some() && self.collision_witnesses.is_some() {
250 if let Some(witnesses) = &mut self.collision_witnesses {
251 witnesses.remove(&cid);
252 }
253 }
254 Ok(removed)
255 }
256
257 pub fn clear(&mut self) {
259 self.store.clear();
260 if let Some(witnesses) = &mut self.collision_witnesses {
261 witnesses.clear();
262 }
263 }
264
265 #[must_use]
267 pub fn len(&self) -> usize {
268 self.store.len()
269 }
270
271 #[must_use]
273 pub fn is_empty(&self) -> bool {
274 self.store.is_empty()
275 }
276
277 #[must_use]
279 pub fn metrics(&self) -> CacheMetrics {
280 CacheMetrics {
281 hits: self.hits.load(Ordering::Relaxed),
282 misses: self.misses.load(Ordering::Relaxed),
283 size: self.store.len(),
284 }
285 }
286
287 pub fn reset_metrics(&self) {
289 self.hits.store(0, Ordering::Relaxed);
290 self.misses.store(0, Ordering::Relaxed);
291 }
292}
293
294impl<K: Contentable, V: Clone, H: Hasher + Eq + StdHash> Clone for ContentStore<K, V, H> {
295 fn clone(&self) -> Self {
296 Self {
297 store: self.store.clone(),
298 collision_witnesses: self.collision_witnesses.clone(),
299 hits: AtomicU64::new(self.hits.load(Ordering::Relaxed)),
300 misses: AtomicU64::new(self.misses.load(Ordering::Relaxed)),
301 _key: std::marker::PhantomData,
302 }
303 }
304}
305
306#[derive(Debug)]
339pub struct KeyedContentStore<
340 K: Contentable,
341 E: StdHash + Eq,
342 V,
343 H: Hasher + Eq + StdHash = DefaultContentHasher,
344> {
345 store: HashMap<ContentId<H>, HashMap<E, V>>,
346 collision_witnesses: Option<HashMap<ContentId<H>, Vec<u8>>>,
347 hits: AtomicU64,
348 misses: AtomicU64,
349 _key: std::marker::PhantomData<K>,
350}
351
352impl<K: Contentable, E: StdHash + Eq + Clone, V, H: Hasher + Eq + StdHash> Default
353 for KeyedContentStore<K, E, V, H>
354{
355 fn default() -> Self {
356 Self::new()
357 }
358}
359
360impl<K: Contentable, E: StdHash + Eq + Clone, V, H: Hasher + Eq + StdHash>
361 KeyedContentStore<K, E, V, H>
362{
363 #[must_use]
365 pub fn new() -> Self {
366 Self {
367 store: HashMap::new(),
368 collision_witnesses: None,
369 hits: AtomicU64::new(0),
370 misses: AtomicU64::new(0),
371 _key: std::marker::PhantomData,
372 }
373 }
374
375 #[must_use]
377 pub fn new_collision_defended() -> Self {
378 Self {
379 store: HashMap::new(),
380 collision_witnesses: Some(HashMap::new()),
381 hits: AtomicU64::new(0),
382 misses: AtomicU64::new(0),
383 _key: std::marker::PhantomData,
384 }
385 }
386
387 #[must_use]
389 pub fn with_capacity(capacity: usize) -> Self {
390 Self {
391 store: HashMap::with_capacity(capacity),
392 collision_witnesses: None,
393 hits: AtomicU64::new(0),
394 misses: AtomicU64::new(0),
395 _key: std::marker::PhantomData,
396 }
397 }
398
399 #[must_use]
401 pub fn with_capacity_collision_defended(capacity: usize) -> Self {
402 Self {
403 store: HashMap::with_capacity(capacity),
404 collision_witnesses: Some(HashMap::with_capacity(capacity)),
405 hits: AtomicU64::new(0),
406 misses: AtomicU64::new(0),
407 _key: std::marker::PhantomData,
408 }
409 }
410
411 #[must_use]
413 pub fn get_with_content_id(&self, cid: &ContentId<H>, extra: &E) -> Option<&V> {
414 match self.store.get(cid).and_then(|inner| inner.get(extra)) {
415 Some(v) => {
416 self.hits.fetch_add(1, Ordering::Relaxed);
417 Some(v)
418 }
419 None => {
420 self.misses.fetch_add(1, Ordering::Relaxed);
421 None
422 }
423 }
424 }
425
426 pub fn get(&self, key: &K, extra: &E) -> Result<Option<&V>, ContentableError> {
433 let cid = key.content_id::<H>()?;
434 if let Some(witnesses) = &self.collision_witnesses {
435 if witnesses.contains_key(&cid) {
436 let bytes = key.to_bytes()?;
437 if witnesses.get(&cid).is_some_and(|stored| stored != &bytes) {
438 return Err(ContentableError::InvalidFormat(
439 "content-id collision detected during keyed get".to_string(),
440 ));
441 }
442 }
443 }
444 Ok(self.get_with_content_id(&cid, extra))
445 }
446
447 pub fn insert_with_content_id(&mut self, cid: ContentId<H>, extra: E, value: V) -> Option<V> {
449 self.store.entry(cid).or_default().insert(extra, value)
450 }
451
452 pub fn insert(&mut self, key: &K, extra: E, value: V) -> Result<Option<V>, ContentableError> {
459 let cid = key.content_id::<H>()?;
460 if let Some(witnesses) = &mut self.collision_witnesses {
461 let bytes = key.to_bytes()?;
462 if let Some(stored) = witnesses.get(&cid) {
463 if stored != &bytes {
464 return Err(ContentableError::InvalidFormat(
465 "content-id collision detected during keyed insert".to_string(),
466 ));
467 }
468 } else {
469 witnesses.insert(cid.clone(), bytes);
470 }
471 }
472 Ok(self.insert_with_content_id(cid, extra, value))
473 }
474
475 pub fn get_or_insert_with<F>(&mut self, key: &K, extra: E, f: F) -> Result<&V, ContentableError>
482 where
483 F: FnOnce() -> V,
484 {
485 let cid = key.content_id::<H>()?;
486 if let Some(witnesses) = &mut self.collision_witnesses {
487 let bytes = key.to_bytes()?;
488 if let Some(stored) = witnesses.get(&cid) {
489 if stored != &bytes {
490 return Err(ContentableError::InvalidFormat(
491 "content-id collision detected during keyed get_or_insert_with".to_string(),
492 ));
493 }
494 } else {
495 witnesses.insert(cid.clone(), bytes);
496 }
497 }
498 match self.store.entry(cid).or_default().entry(extra) {
499 std::collections::hash_map::Entry::Occupied(entry) => {
500 self.hits.fetch_add(1, Ordering::Relaxed);
501 Ok(entry.into_mut())
502 }
503 std::collections::hash_map::Entry::Vacant(entry) => {
504 self.misses.fetch_add(1, Ordering::Relaxed);
505 Ok(entry.insert(f()))
506 }
507 }
508 }
509
510 pub fn contains(&self, key: &K, extra: &E) -> Result<bool, ContentableError> {
516 let cid = key.content_id::<H>()?;
517 Ok(self.contains_with_content_id(&cid, extra))
518 }
519
520 #[must_use]
522 pub fn contains_with_content_id(&self, cid: &ContentId<H>, extra: &E) -> bool {
523 self.store
524 .get(cid)
525 .is_some_and(|inner| inner.contains_key(extra))
526 }
527
528 pub fn remove(&mut self, key: &K, extra: &E) -> Result<Option<V>, ContentableError> {
534 let cid = key.content_id::<H>()?;
535 if let Some(inner) = self.store.get_mut(&cid) {
536 let removed = inner.remove(extra);
537 if inner.is_empty() {
538 self.store.remove(&cid);
539 if let Some(witnesses) = &mut self.collision_witnesses {
540 witnesses.remove(&cid);
541 }
542 }
543 Ok(removed)
544 } else {
545 Ok(None)
546 }
547 }
548
549 pub fn clear(&mut self) {
551 self.store.clear();
552 if let Some(witnesses) = &mut self.collision_witnesses {
553 witnesses.clear();
554 }
555 }
556
557 #[must_use]
559 pub fn len(&self) -> usize {
560 self.store.values().map(HashMap::len).sum()
561 }
562
563 #[must_use]
565 pub fn is_empty(&self) -> bool {
566 self.len() == 0
567 }
568
569 #[must_use]
571 pub fn metrics(&self) -> CacheMetrics {
572 CacheMetrics {
573 hits: self.hits.load(Ordering::Relaxed),
574 misses: self.misses.load(Ordering::Relaxed),
575 size: self.store.len(),
576 }
577 }
578
579 pub fn reset_metrics(&self) {
581 self.hits.store(0, Ordering::Relaxed);
582 self.misses.store(0, Ordering::Relaxed);
583 }
584}
585
586impl<K: Contentable, E: StdHash + Eq + Clone, V: Clone, H: Hasher + Eq + StdHash> Clone
587 for KeyedContentStore<K, E, V, H>
588{
589 fn clone(&self) -> Self {
590 Self {
591 store: self.store.clone(),
592 collision_witnesses: self.collision_witnesses.clone(),
593 hits: AtomicU64::new(self.hits.load(Ordering::Relaxed)),
594 misses: AtomicU64::new(self.misses.load(Ordering::Relaxed)),
595 _key: std::marker::PhantomData,
596 }
597 }
598}
599
600#[cfg(test)]
601mod tests {
602 use super::*;
603 use crate::content_id::Hasher;
604 use crate::{GlobalType, Label, LocalTypeR};
605
606 #[derive(Clone, Default, PartialEq, Eq, Hash)]
607 struct ConstantHasher;
608
609 impl Hasher for ConstantHasher {
610 type Digest = [u8; 1];
611 const HASH_SIZE: usize = 1;
612
613 fn digest(_data: &[u8]) -> Self::Digest {
614 [0u8]
615 }
616
617 fn algorithm_name() -> &'static str {
618 "constant"
619 }
620 }
621
622 #[test]
623 fn test_content_store_basic() {
624 let mut store: ContentStore<GlobalType, LocalTypeR> = ContentStore::new();
625
626 let global = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
627 let local = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
628
629 assert!(store.is_empty());
631 assert_eq!(store.get(&global).unwrap(), None);
632
633 store.insert(&global, local.clone()).unwrap();
635 assert_eq!(store.len(), 1);
636
637 assert_eq!(store.get(&global).unwrap(), Some(&local));
639
640 let metrics = store.metrics();
642 assert_eq!(metrics.hits, 1); assert_eq!(metrics.misses, 1); }
645
646 #[test]
647 fn test_content_store_alpha_equivalence() {
648 let mut store: ContentStore<GlobalType, String> = ContentStore::new();
649
650 let g1 = GlobalType::mu(
652 "x",
653 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
654 );
655 let g2 = GlobalType::mu(
656 "y",
657 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
658 );
659
660 store.insert(&g1, "result".to_string()).unwrap();
661
662 assert_eq!(store.get(&g2).unwrap(), Some(&"result".to_string()));
664 }
665
666 #[test]
667 fn test_content_store_get_or_insert_with() {
668 let mut store: ContentStore<GlobalType, i32> = ContentStore::new();
669 let global = GlobalType::End;
670
671 let mut computed = false;
672 let value = store
673 .get_or_insert_with(&global, || {
674 computed = true;
675 42
676 })
677 .unwrap();
678 assert_eq!(*value, 42);
679 assert!(computed);
680
681 computed = false;
683 let value = store
684 .get_or_insert_with(&global, || {
685 computed = true;
686 99
687 })
688 .unwrap();
689 assert_eq!(*value, 42); assert!(!computed); let metrics = store.metrics();
693 assert_eq!(metrics.hits, 1);
694 assert_eq!(metrics.misses, 1);
695 }
696
697 #[test]
698 fn test_keyed_content_store() {
699 let mut store: KeyedContentStore<GlobalType, String, LocalTypeR> = KeyedContentStore::new();
700
701 let global = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
702 let local_a = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
703 let local_b = LocalTypeR::recv("A", Label::new("msg"), LocalTypeR::End);
704
705 store
707 .insert(&global, "A".to_string(), local_a.clone())
708 .unwrap();
709 store
710 .insert(&global, "B".to_string(), local_b.clone())
711 .unwrap();
712
713 assert_eq!(store.len(), 2);
714 assert_eq!(
715 store.get(&global, &"A".to_string()).unwrap(),
716 Some(&local_a)
717 );
718 assert_eq!(
719 store.get(&global, &"B".to_string()).unwrap(),
720 Some(&local_b)
721 );
722 assert_eq!(store.get(&global, &"C".to_string()).unwrap(), None);
723 }
724
725 #[test]
726 fn test_cache_metrics() {
727 let mut store: ContentStore<GlobalType, i32> = ContentStore::new();
728 let g1 = GlobalType::End;
729 let g2 = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
730
731 store.get(&g1).unwrap();
733 store.get(&g2).unwrap();
734
735 store.insert(&g1, 1).unwrap();
737
738 store.get(&g1).unwrap();
740 store.get(&g1).unwrap();
741
742 store.get(&g2).unwrap();
744
745 let metrics = store.metrics();
746 assert_eq!(metrics.misses, 3); assert_eq!(metrics.hits, 2); assert!((metrics.hit_rate() - 40.0).abs() < 0.01); }
750
751 #[test]
752 fn test_content_store_clear() {
753 let mut store: ContentStore<GlobalType, i32> = ContentStore::new();
754
755 store.insert(&GlobalType::End, 1).unwrap();
756 store
757 .insert(
758 &GlobalType::send("A", "B", Label::new("msg"), GlobalType::End),
759 2,
760 )
761 .unwrap();
762
763 assert_eq!(store.len(), 2);
764
765 store.clear();
766 assert!(store.is_empty());
767 }
768
769 #[test]
770 fn test_content_store_remove() {
771 let mut store: ContentStore<GlobalType, i32> = ContentStore::new();
772 let global = GlobalType::End;
773
774 store.insert(&global, 42).unwrap();
775 assert!(store.contains(&global).unwrap());
776
777 let removed = store.remove(&global).unwrap();
778 assert_eq!(removed, Some(42));
779 assert!(!store.contains(&global).unwrap());
780 }
781
782 #[test]
783 fn test_collision_defense_rejects_hash_alias_in_content_store() {
784 let mut store: ContentStore<GlobalType, i32, ConstantHasher> =
785 ContentStore::new_collision_defended();
786 let g1 = GlobalType::send("A", "B", Label::new("x"), GlobalType::End);
787 let g2 = GlobalType::send("A", "B", Label::new("y"), GlobalType::End);
788 store.insert(&g1, 1).expect("first insert should succeed");
789 let result = store.insert(&g2, 2);
790 assert!(matches!(result, Err(ContentableError::InvalidFormat(_))));
791 }
792
793 #[test]
794 fn test_collision_defense_rejects_hash_alias_in_keyed_store() {
795 let mut store: KeyedContentStore<GlobalType, String, i32, ConstantHasher> =
796 KeyedContentStore::new_collision_defended();
797 let g1 = GlobalType::send("A", "B", Label::new("x"), GlobalType::End);
798 let g2 = GlobalType::send("A", "B", Label::new("y"), GlobalType::End);
799 store
800 .insert(&g1, "A".to_string(), 1)
801 .expect("first insert should succeed");
802 let result = store.insert(&g2, "B".to_string(), 2);
803 assert!(matches!(result, Err(ContentableError::InvalidFormat(_))));
804 }
805}