124124//! - [`HandleStore`] is **not** thread-safe (uses `Cell` for metrics)
125125//! - [`ConcurrentHandleStore`] is `Send + Sync` via `parking_lot::RwLock`
126126//!
127+ //! ## Security Considerations
128+ //!
129+ //! - **Hasher:** backed by `FxHashMap`, which is fast but **not** HashDoS-resistant.
130+ //! Handles (`H`) must come from a trusted source — typically a
131+ //! [`KeyInterner`](crate::ds::KeyInterner) producing monotonic `u64` IDs.
132+ //! Passing attacker-controlled hashable values directly as handles is
133+ //! unsupported.
134+ //! - **Capacity:** [`HandleStore::new`] panics on allocator failure. Use
135+ //! [`HandleStore::try_new`] (and the concurrent equivalent) when `capacity`
136+ //! originates from untrusted input.
137+ //! - **Sensitive values:** dropped values are not zeroed. Wrap `V` in
138+ //! `zeroize::Zeroizing` (or equivalent) when caching secrets.
139+ //! - **Lock poisoning:** `parking_lot::RwLock` does not poison on panic.
140+ //! Under `panic = "unwind"`, avoid panicking `Drop` impls on `V`.
141+ //!
127142//! ## Implementation Notes
128143//!
129144//! - Handles must remain stable for the lifetime of stored entries
@@ -197,6 +212,10 @@ impl StoreCounters {
197212 self . removes . set ( self . removes . get ( ) + 1 ) ;
198213 }
199214
215+ fn add_removes ( & self , n : u64 ) {
216+ self . removes . set ( self . removes . get ( ) . saturating_add ( n) ) ;
217+ }
218+
200219 fn inc_eviction ( & self ) {
201220 self . evictions . set ( self . evictions . get ( ) + 1 ) ;
202221 }
@@ -256,6 +275,10 @@ impl ConcurrentStoreCounters {
256275 self . removes . fetch_add ( 1 , Ordering :: Relaxed ) ;
257276 }
258277
278+ fn add_removes ( & self , n : u64 ) {
279+ self . removes . fetch_add ( n, Ordering :: Relaxed ) ;
280+ }
281+
259282 fn inc_eviction ( & self ) {
260283 self . evictions . fetch_add ( 1 , Ordering :: Relaxed ) ;
261284 }
@@ -317,6 +340,9 @@ where
317340{
318341 /// Creates a new handle store with the specified maximum capacity.
319342 ///
343+ /// Panics on allocator failure. Use [`try_new`](Self::try_new) when
344+ /// `capacity` is derived from untrusted input.
345+ ///
320346 /// # Example
321347 ///
322348 /// ```
@@ -334,6 +360,35 @@ where
334360 }
335361 }
336362
363+ /// Fallible constructor for [`new`](Self::new).
364+ ///
365+ /// Reserves space for `capacity` entries without panicking on allocator
366+ /// failure. Preferred when `capacity` comes from configuration, RPC input,
367+ /// or any source that is not fully trusted.
368+ ///
369+ /// # Errors
370+ ///
371+ /// Returns [`TryReserveError`](std::collections::TryReserveError) if the
372+ /// allocator cannot reserve capacity for `capacity` entries.
373+ ///
374+ /// # Example
375+ ///
376+ /// ```
377+ /// use cachekit::store::handle::HandleStore;
378+ ///
379+ /// let store: HandleStore<u64, i32> = HandleStore::try_new(1000).unwrap();
380+ /// assert_eq!(store.capacity(), 1000);
381+ /// ```
382+ pub fn try_new ( capacity : usize ) -> Result < Self , std:: collections:: TryReserveError > {
383+ let mut map: FxHashMap < H , Arc < V > > = FxHashMap :: with_hasher ( Default :: default ( ) ) ;
384+ map. try_reserve ( capacity) ?;
385+ Ok ( Self {
386+ map,
387+ capacity,
388+ metrics : StoreCounters :: default ( ) ,
389+ } )
390+ }
391+
337392 /// Returns a clone of the value for the given handle.
338393 ///
339394 /// Updates hit/miss metrics. Use [`peek`](Self::peek) to avoid metric updates.
@@ -471,9 +526,12 @@ where
471526
472527 /// Removes all entries from the store.
473528 ///
474- /// Does not update metrics. Capacity remains unchanged.
529+ /// Attributes the cleared entries to the `removes` metric so audit trails
530+ /// reflect that entries left the store. Capacity remains unchanged.
475531 pub fn clear ( & mut self ) {
532+ let removed = self . map . len ( ) as u64 ;
476533 self . map . clear ( ) ;
534+ self . metrics . add_removes ( removed) ;
477535 }
478536
479537 /// Returns the current number of entries.
@@ -616,6 +674,9 @@ where
616674{
617675 /// Creates a new concurrent handle store with the specified capacity.
618676 ///
677+ /// Panics on allocator failure. Use [`try_new`](Self::try_new) when
678+ /// `capacity` is derived from untrusted input.
679+ ///
619680 /// # Example
620681 ///
621682 /// ```
@@ -637,6 +698,37 @@ where
637698 }
638699 }
639700
701+ /// Fallible constructor for [`new`](Self::new).
702+ ///
703+ /// Reserves space for `capacity` entries without panicking on allocator
704+ /// failure. Preferred when `capacity` comes from configuration, RPC input,
705+ /// or any source that is not fully trusted.
706+ ///
707+ /// # Errors
708+ ///
709+ /// Returns [`TryReserveError`](std::collections::TryReserveError) if the
710+ /// allocator cannot reserve capacity for `capacity` entries.
711+ ///
712+ /// # Example
713+ ///
714+ /// ```
715+ /// use cachekit::store::handle::ConcurrentHandleStore;
716+ /// use cachekit::store::traits::ConcurrentStoreRead;
717+ ///
718+ /// let store: ConcurrentHandleStore<u64, String> =
719+ /// ConcurrentHandleStore::try_new(1000).unwrap();
720+ /// assert_eq!(store.capacity(), 1000);
721+ /// ```
722+ pub fn try_new ( capacity : usize ) -> Result < Self , std:: collections:: TryReserveError > {
723+ let mut map: FxHashMap < H , Arc < V > > = FxHashMap :: with_hasher ( Default :: default ( ) ) ;
724+ map. try_reserve ( capacity) ?;
725+ Ok ( Self {
726+ map : RwLock :: new ( map) ,
727+ capacity,
728+ metrics : ConcurrentStoreCounters :: default ( ) ,
729+ } )
730+ }
731+
640732 /// Records an eviction in the metrics.
641733 ///
642734 /// Thread-safe via atomic increment. Call when the policy evicts an entry.
@@ -758,9 +850,13 @@ where
758850
759851 /// Removes all entries.
760852 ///
761- /// Acquires a write lock. Does not update metrics.
853+ /// Acquires a write lock. Attributes the cleared entries to the `removes`
854+ /// metric so audit trails reflect that entries left the store.
762855 fn clear ( & self ) {
763- self . map . write ( ) . clear ( ) ;
856+ let mut map = self . map . write ( ) ;
857+ let removed = map. len ( ) as u64 ;
858+ map. clear ( ) ;
859+ self . metrics . add_removes ( removed) ;
764860 }
765861}
766862
@@ -843,6 +939,53 @@ mod tests {
843939 assert ! ( !store. contains( & 1u64 ) ) ;
844940 }
845941
942+ #[ test]
943+ fn handle_store_clear_attributes_removes ( ) {
944+ let mut store: HandleStore < u64 , ( ) > = HandleStore :: new ( 4 ) ;
945+ store. try_insert ( 1u64 , Arc :: new ( ( ) ) ) . unwrap ( ) ;
946+ store. try_insert ( 2u64 , Arc :: new ( ( ) ) ) . unwrap ( ) ;
947+ store. try_insert ( 3u64 , Arc :: new ( ( ) ) ) . unwrap ( ) ;
948+
949+ store. clear ( ) ;
950+
951+ assert_eq ! ( store. len( ) , 0 ) ;
952+ assert_eq ! ( store. metrics( ) . removes, 3 ) ;
953+ }
954+
955+ #[ test]
956+ fn handle_store_try_new_reserves_capacity ( ) {
957+ let store: HandleStore < u64 , ( ) > = HandleStore :: try_new ( 0 ) . expect ( "try_new(0)" ) ;
958+ assert_eq ! ( store. capacity( ) , 0 ) ;
959+ assert_eq ! ( store. len( ) , 0 ) ;
960+
961+ let store: HandleStore < u64 , ( ) > = HandleStore :: try_new ( 128 ) . expect ( "try_new(128)" ) ;
962+ assert_eq ! ( store. capacity( ) , 128 ) ;
963+ assert_eq ! ( store. len( ) , 0 ) ;
964+ }
965+
966+ #[ cfg( feature = "concurrency" ) ]
967+ #[ test]
968+ fn concurrent_handle_store_clear_attributes_removes ( ) {
969+ let store: ConcurrentHandleStore < u64 , ( ) > = ConcurrentHandleStore :: new ( 4 ) ;
970+ store. try_insert ( 1u64 , Arc :: new ( ( ) ) ) . unwrap ( ) ;
971+ store. try_insert ( 2u64 , Arc :: new ( ( ) ) ) . unwrap ( ) ;
972+ store. try_insert ( 3u64 , Arc :: new ( ( ) ) ) . unwrap ( ) ;
973+
974+ store. clear ( ) ;
975+
976+ assert_eq ! ( store. len( ) , 0 ) ;
977+ assert_eq ! ( store. metrics( ) . removes, 3 ) ;
978+ }
979+
980+ #[ cfg( feature = "concurrency" ) ]
981+ #[ test]
982+ fn concurrent_handle_store_try_new_reserves_capacity ( ) {
983+ let store: ConcurrentHandleStore < u64 , ( ) > =
984+ ConcurrentHandleStore :: try_new ( 128 ) . expect ( "try_new(128)" ) ;
985+ assert_eq ! ( store. capacity( ) , 128 ) ;
986+ assert_eq ! ( store. len( ) , 0 ) ;
987+ }
988+
846989 #[ test]
847990 fn handle_store_metrics_counts ( ) {
848991 let mut store = HandleStore :: new ( 2 ) ;
0 commit comments