Skip to main content

structured_asn1/memory/
map.rs

1//! Accounted storage and pre-admitted hashing for the existing hashbrown engine.
2
3use core::borrow::Borrow;
4use core::fmt;
5use core::hash::{BuildHasher, Hash};
6
7use hashbrown::hash_map::RawEntryMut;
8use hashbrown::{DefaultHashBuilder, HashMap, TryReserveError};
9
10use super::allocator::AccountAllocator;
11use super::{Memory, MemoryError, Work};
12
13#[cfg(test)]
14mod tests;
15
16/// A refused in-place filter, preserving the predicate's own cause.
17#[derive(Debug, PartialEq, Eq)]
18pub enum RetainError<E> {
19    /// The map could not admit its traversal or erase metadata.
20    Resource(MemoryError),
21    /// The caller's predicate refused further processing.
22    Predicate(E),
23}
24
25impl From<RetainError<MemoryError>> for MemoryError {
26    fn from(error: RetainError<MemoryError>) -> Self {
27        match error {
28            RetainError::Resource(reason) | RetainError::Predicate(reason) => reason,
29        }
30    }
31}
32
33/// A trusted key type's upper bound for hashing and equality work.
34///
35/// Implementations must cover all bytes examined by Hash and Eq. This is a
36/// contract for caller-provided code, not a value an untrusted document supplies.
37/// The bound must remain unchanged and successful while a key is stored, just
38/// as Hash and Eq must remain stable. Accounting retains this bound for rehashes.
39pub trait HashWork: Hash + Eq {
40    /// Work units for one hash/equality traversal, including constant overhead.
41    fn hash_work(&self) -> Result<u64, MemoryError>;
42}
43
44impl HashWork for usize {
45    fn hash_work(&self) -> Result<u64, MemoryError> {
46        Ok(1)
47    }
48}
49
50impl HashWork for u8 {
51    fn hash_work(&self) -> Result<u64, MemoryError> {
52        Ok(1)
53    }
54}
55
56impl HashWork for (usize, usize, usize) {
57    fn hash_work(&self) -> Result<u64, MemoryError> {
58        Ok(3)
59    }
60}
61
62impl HashWork for &[u8] {
63    fn hash_work(&self) -> Result<u64, MemoryError> {
64        Ok(self.len() as u64 + 1)
65    }
66}
67
68impl HashWork for str {
69    fn hash_work(&self) -> Result<u64, MemoryError> {
70        (self.len() as u64)
71            .checked_add(1)
72            .ok_or(MemoryError::WorkExceeded)
73    }
74}
75
76impl HashWork for crate::Oid<'_> {
77    fn hash_work(&self) -> Result<u64, MemoryError> {
78        Ok(self.as_bytes().len() as u64 + 1)
79    }
80}
81
82impl HashWork for (crate::Oid<'_>, crate::Oid<'_>) {
83    fn hash_work(&self) -> Result<u64, MemoryError> {
84        self.0
85            .hash_work()?
86            .checked_add(self.1.hash_work()?)
87            .ok_or(MemoryError::WorkExceeded)
88    }
89}
90
91/// A hash map whose backing allocation and operations share one resource domain.
92///
93/// Hashbrown supplies bucket/probing/SIMD machinery. Reservations include its
94/// control bytes and alignment, not merely the initialized entries. Hashing and
95/// actual collision comparisons are admitted before examining candidate keys.
96pub struct OwnedMap<K: HashWork, V, W: Work = Memory> {
97    values: HashMap<K, V, DefaultHashBuilder, AccountAllocator<W>>,
98    execution: W,
99    key_work: u64,
100    maximum_capacity: usize,
101}
102
103impl<K: HashWork + fmt::Debug, V: fmt::Debug, W: Work> fmt::Debug for OwnedMap<K, V, W> {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        self.values.fmt(f)
106    }
107}
108
109impl<K: HashWork, V, W: Work> OwnedMap<K, V, W> {
110    pub(super) fn new(execution: W) -> Self {
111        Self {
112            values: HashMap::with_hasher_in(
113                DefaultHashBuilder::default(),
114                AccountAllocator::new(execution.clone()),
115            ),
116            execution,
117            key_work: 0,
118            maximum_capacity: 0,
119        }
120    }
121
122    /// The shared account retained until the table is deallocated.
123    #[must_use]
124    pub fn memory(&self) -> &Memory {
125        self.execution.memory()
126    }
127
128    /// Number of initialized entries.
129    #[must_use]
130    pub fn len(&self) -> usize {
131        self.values.len()
132    }
133
134    /// Whether there are no entries.
135    #[must_use]
136    pub fn is_empty(&self) -> bool {
137        self.values.is_empty()
138    }
139
140    /// Borrowed entries after admitting a full control-bucket traversal in
141    /// the current execution domain, independently of retained storage.
142    /// Sparse holes count too. Partial consumption retains the conservative
143    /// prepayment; the opaque iterator exposes no cloning/restart operation.
144    ///
145    /// # Examples
146    /// ```
147    /// # extern crate alloc;
148    /// # use alloc::sync::Arc;
149    /// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
150    /// # let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
151    /// let mut map = memory.map();
152    /// map.try_insert(1usize, 42)?;
153    /// let mut entries = map.try_iter(&memory)?;
154    /// assert_eq!(entries.next(), Some((&1, &42)));
155    /// # Ok::<(), MemoryError>(())
156    /// ```
157    pub fn try_iter<'a, E: Work>(
158        &'a self,
159        execution: &E,
160    ) -> Result<impl Iterator<Item = (&'a K, &'a V)> + 'a + use<'a, K, V, W, E>, MemoryError> {
161        execution.charge_work(self.buckets()?)?;
162        Ok(self.values.iter())
163    }
164
165    /// Retains entries selected by a fallible predicate, without allocating.
166    ///
167    /// The supplied current execution admits the entire control-bucket scan,
168    /// including sparse holes, before the first predicate call. Every erase
169    /// separately admits its metadata work before removal. The predicate must
170    /// admit its own data-dependent work; arbitrary key/value destructors are
171    /// likewise caller-owned code. No key hashing or equality is performed.
172    ///
173    /// Entries are visited in unspecified order. On the first refusal no more
174    /// predicates or key-work metadata callbacks run; the engine finishes its
175    /// already admitted scan while retaining all remaining entries. Earlier
176    /// removals and predicate value edits, including an edit made by a failing
177    /// predicate, remain. Refusal never reports a successful partial filter.
178    ///
179    /// The original allocation, capacity charge and storage account remain
180    /// unchanged. Future growth still uses the map's original execution.
181    /// Filtering under another execution does not rebind that allocator.
182    ///
183    /// # Examples
184    /// ```
185    /// # extern crate alloc;
186    /// # use alloc::sync::Arc;
187    /// use structured_asn1::memory::{Memory, MemoryAccount};
188    /// let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
189    /// let mut map = memory.map();
190    /// map.try_insert(1usize, 10).unwrap();
191    /// map.try_insert(2usize, 20).unwrap();
192    /// map.try_retain(&memory, |key, value| {
193    ///     *value += 1;
194    ///     Ok::<_, core::convert::Infallible>(*key == 2)
195    /// }).unwrap();
196    /// assert_eq!(map.len(), 1);
197    /// assert_eq!(map.get(&2, &memory).unwrap(), Some(&21));
198    /// ```
199    pub fn try_retain<E>(
200        &mut self,
201        execution: &impl Work,
202        mut predicate: impl FnMut(&K, &mut V) -> Result<bool, E>,
203    ) -> Result<(), RetainError<E>> {
204        let buckets = self.buckets().map_err(RetainError::Resource)?;
205        execution
206            .charge_work(buckets)
207            .map_err(RetainError::Resource)?;
208        let mut failure = None;
209        let key_work = &mut self.key_work;
210        self.values.retain(|key, value| {
211            if failure.is_some() {
212                return true;
213            }
214            match predicate(key, value) {
215                Ok(true) => true,
216                Err(reason) => {
217                    failure = Some(RetainError::Predicate(reason));
218                    true
219                }
220                Ok(false) => {
221                    // Pinned hashbrown 0.17.1 RawTableInner::erase loads the
222                    // adjacent two control groups, then drops the pair in place.
223                    // It neither hashes keys nor moves entries to another table.
224                    if let Err(reason) = execution.charge_work(2) {
225                        failure = Some(RetainError::Resource(reason));
226                        return true;
227                    }
228                    let removed = match key.hash_work() {
229                        Ok(work) => work,
230                        Err(reason) => {
231                            failure = Some(RetainError::Resource(reason));
232                            return true;
233                        }
234                    };
235                    // HashWork's stored bound is stable by contract. Update
236                    // before hashbrown erases and invokes a possibly panicking Drop.
237                    debug_assert!(*key_work >= removed);
238                    *key_work -= removed;
239                    false
240                }
241            }
242        });
243        match failure {
244            Some(reason) => Err(reason),
245            None => Ok(()),
246        }
247    }
248
249    fn buckets(&self) -> Result<u64, MemoryError> {
250        // Pinned hashbrown 0.17.1 raw::bucket_mask_to_capacity: tables with
251        // four/eight buckets keep one empty slot; larger tables use 7/8 of
252        // the buckets. Maximum capacity records each table immediately after
253        // reserve, before tombstones can reduce the public capacity value.
254        match self.maximum_capacity {
255            0 => Ok(0),
256            capacity @ 1..=7 => Ok(capacity as u64 + 1),
257            capacity => (capacity as u64 / 7)
258                .checked_mul(8)
259                .ok_or(MemoryError::WorkExceeded),
260        }
261    }
262
263    fn control_work(&self) -> Result<u64, MemoryError> {
264        let buckets = self.buckets()?;
265        // The pinned engine's narrowest control group is four bytes. Wider
266        // scalar/SIMD groups perform the same fixed group operations. One unit
267        // admits one group visit; triangular probing visits each group once.
268        Ok(buckets / 4)
269    }
270
271    fn rehash_work(&self) -> Result<u64, MemoryError> {
272        // reserve(1) can double the current buckets or rehash in place. Every
273        // retained key is hashed once; each slot search may visit all groups.
274        // In-place rehash additionally scans every bucket and swaps entries.
275        let buckets = self.buckets()?;
276        let groups = buckets.checked_mul(2).ok_or(MemoryError::WorkExceeded)? / 4;
277        let moves = (core::mem::size_of::<(K, V)>() as u64)
278            .checked_mul(2)
279            .ok_or(MemoryError::WorkExceeded)?;
280        groups
281            .checked_add(moves)
282            .and_then(|per_entry| per_entry.checked_mul(self.values.len() as u64))
283            .and_then(|work| work.checked_add(self.key_work))
284            .and_then(|work| {
285                buckets
286                    .checked_mul(2)
287                    .and_then(|scan| work.checked_add(scan))
288            })
289            .ok_or(MemoryError::WorkExceeded)
290    }
291
292    fn hash_key<Q: HashWork + ?Sized, E: Work>(
293        &self,
294        key: &Q,
295        execution: &E,
296    ) -> Result<u64, MemoryError> {
297        let work = key
298            .hash_work()?
299            .checked_add(self.control_work()?)
300            .ok_or(MemoryError::WorkExceeded)?;
301        execution.charge_work(work)?;
302        Ok(self.values.hasher().hash_one(key))
303    }
304
305    /// Looks up a key after admitting its complete possible probe work.
306    /// The current execution domain pays independently of stored capacity.
307    ///
308    /// # Examples
309    /// ```
310    /// # extern crate alloc;
311    /// # use alloc::sync::Arc;
312    /// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
313    /// # let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
314    /// # let mut map = memory.map();
315    /// # map.try_insert(1usize, 42)?;
316    /// assert_eq!(map.get(&1, &memory)?, Some(&42));
317    /// assert_eq!(map.get(&2, &memory)?, None);
318    /// # Ok::<(), MemoryError>(())
319    /// ```
320    pub fn get(&self, key: &K, execution: &impl Work) -> Result<Option<&V>, MemoryError> {
321        self.get_key_value_borrowed(key, execution)
322            .map(|entry| entry.map(|(_, value)| value))
323    }
324
325    /// Finds the canonical stored key and value using an allocation-free query.
326    ///
327    /// Owned and borrowed forms must have identical Hash/Eq semantics, as
328    /// required by [`Borrow`]. Current work admits query hashing, table probes
329    /// and every candidate comparison before executing them. The stored key's
330    /// [`HashWork`] bound must also cover its `Borrow<Q>` operation, which must
331    /// not allocate. A refusal returns `Err`, never successful absence or a
332    /// synthetic match. No table/account is rebound and no query key is copied.
333    ///
334    /// Returning the canonical key lets an interner clone its existing shared
335    /// owner without a second lookup or a temporary owned query allocation.
336    ///
337    /// ```
338    /// use std::sync::Arc;
339    /// use structured_asn1::memory::{Memory, MemoryAccount};
340    /// let storage = Memory::new(Arc::new(MemoryAccount::new(4096, 4096)));
341    /// let mut map = storage.map();
342    /// map.try_insert(7usize, "retained")?;
343    /// let query = Memory::new(Arc::new(MemoryAccount::new(0, 1024)));
344    /// assert_eq!(map.get_key_value_borrowed(&7, &query)?, Some((&7, &"retained")));
345    /// # Ok::<(), structured_asn1::memory::MemoryError>(())
346    /// ```
347    pub fn get_key_value_borrowed<Q: HashWork + ?Sized>(
348        &self,
349        key: &Q,
350        execution: &impl Work,
351    ) -> Result<Option<(&K, &V)>, MemoryError>
352    where
353        K: Borrow<Q>,
354    {
355        let hash = self.hash_key(key, execution)?;
356        let mut failure = None;
357        let entry = self.values.raw_entry().from_hash(hash, |candidate| {
358            equivalent(execution, key, candidate, &mut failure)
359        });
360        if let Some(reason) = failure {
361            return Err(reason);
362        }
363        Ok(entry)
364    }
365
366    /// Looks up an entry for mutation without exposing table growth.
367    /// Hashing, probing and equality use the supplied execution domain.
368    ///
369    /// # Examples
370    /// ```
371    /// # extern crate alloc;
372    /// # use alloc::sync::Arc;
373    /// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
374    /// # let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
375    /// # let mut map = memory.map();
376    /// # map.try_insert(1usize, 42)?;
377    /// if let Some(value) = map.get_mut(&1, &memory)? { *value = 7; }
378    /// assert_eq!(map.get(&1, &memory)?, Some(&7));
379    /// # Ok::<(), MemoryError>(())
380    /// ```
381    pub fn get_mut(
382        &mut self,
383        key: &K,
384        execution: &impl Work,
385    ) -> Result<Option<&mut V>, MemoryError> {
386        self.get_mut_borrowed(key, execution)
387    }
388
389    /// Finds a mutable value using an allocation-free borrowed query.
390    ///
391    /// Uses the same Hash/Eq/`Borrow` and [`HashWork`] requirements as
392    /// [`Self::get_key_value_borrowed`]. Current work admits hashing, probes and
393    /// each candidate comparison before exposing a mutable value. Refusal is
394    /// `Err`, never absence or a synthetic match. No owned query, second lookup
395    /// or table/account rebinding is performed.
396    pub fn get_mut_borrowed<Q: HashWork + ?Sized>(
397        &mut self,
398        key: &Q,
399        execution: &impl Work,
400    ) -> Result<Option<&mut V>, MemoryError>
401    where
402        K: Borrow<Q>,
403    {
404        let hash = self.hash_key(key, execution)?;
405        let mut failure = None;
406        let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
407            equivalent(execution, key, candidate, &mut failure)
408        });
409        if let Some(reason) = failure {
410            return Err(reason);
411        }
412        Ok(match entry {
413            RawEntryMut::Occupied(entry) => Some(entry.into_mut()),
414            RawEntryMut::Vacant(_) => None,
415        })
416    }
417
418    /// Inserts a value fallibly, or replaces the value for an existing key.
419    /// This storage mutation runs in the collection's original resource
420    /// domain, including hashing, rehashing, allocation and copying work.
421    /// To mutate under a different domain, build a new collection there from
422    /// explicitly admitted borrowed entries; retained capacity is not rebound.
423    pub fn try_insert(&mut self, key: K, value: V) -> Result<Option<V>, MemoryError> {
424        let hash = self.hash_key(&key, &self.execution)?;
425        let mut failure = None;
426        let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
427            equivalent(&self.execution, &key, candidate, &mut failure)
428        });
429        if let Some(reason) = failure {
430            return Err(reason);
431        }
432        if let RawEntryMut::Occupied(entry) = entry {
433            let moves = (core::mem::size_of::<V>() as u64)
434                .checked_mul(2)
435                .ok_or(MemoryError::WorkExceeded)?;
436            self.execution.charge_work(moves)?;
437            return Ok(Some(core::mem::replace(entry.into_mut(), value)));
438        }
439        let key_work = self
440            .key_work
441            .checked_add(key.hash_work()?)
442            .ok_or(MemoryError::WorkExceeded)?;
443        if self.values.len() == self.values.capacity() {
444            // Include collision-dependent slot searches, not just key hashes.
445            self.execution.charge_work(self.rehash_work()?)?;
446            self.values.allocator().clear_failure();
447            self.values.try_reserve(1).map_err(|error| match error {
448                TryReserveError::CapacityOverflow => MemoryError::CapacityOverflow,
449                TryReserveError::AllocError { .. } => self.values.allocator().failure(),
450            })?;
451            self.maximum_capacity = self.maximum_capacity.max(self.values.capacity());
452        }
453        // Reuse the already computed hash after reserve. No key equality is
454        // needed: the first lookup established absence and resize preserves keys.
455        // raw_entry searches for a match; insert_hashed_nocheck subsequently
456        // searches for an insertion slot. Admit both control traversals.
457        self.execution.charge_work(
458            self.control_work()?
459                .checked_mul(2)
460                .and_then(|work| work.checked_add(core::mem::size_of::<(K, V)>() as u64))
461                .ok_or(MemoryError::WorkExceeded)?,
462        )?;
463        let RawEntryMut::Vacant(entry) = self.values.raw_entry_mut().from_hash(hash, |_| false)
464        else {
465            unreachable!("a predicate that always refuses cannot find an entry");
466        };
467        entry.insert_hashed_nocheck(hash, key, value);
468        self.key_work = key_work;
469        Ok(None)
470    }
471
472    /// Removes an entry, retaining table capacity and its storage charge.
473    /// The supplied execution domain pays for lookup and removal before mutation.
474    ///
475    /// # Examples
476    /// ```
477    /// # extern crate alloc;
478    /// # use alloc::sync::Arc;
479    /// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
480    /// # let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
481    /// # let mut map = memory.map();
482    /// # map.try_insert(1usize, 42)?;
483    /// let capacity_charge = memory.used();
484    /// assert_eq!(map.remove(&1, &memory)?, Some(42));
485    /// assert_eq!(memory.used(), capacity_charge);
486    /// # Ok::<(), MemoryError>(())
487    /// ```
488    pub fn remove(&mut self, key: &K, execution: &impl Work) -> Result<Option<V>, MemoryError> {
489        self.remove_borrowed(key, execution)
490    }
491
492    /// Removes a value using an allocation-free borrowed query.
493    ///
494    /// Uses the same Hash/Eq/`Borrow` and [`HashWork`] requirements as
495    /// [`Self::get_key_value_borrowed`]. Current work admits lookup and erase
496    /// metadata/value movement before mutation. Refusal leaves the entry intact.
497    /// The original table capacity and allocator remain retained; the removed
498    /// key is dropped and the value transfers its independent ownership.
499    /// Arbitrary key/value destructor work remains caller-owned.
500    pub fn remove_borrowed<Q: HashWork + ?Sized>(
501        &mut self,
502        key: &Q,
503        execution: &impl Work,
504    ) -> Result<Option<V>, MemoryError>
505    where
506        K: Borrow<Q>,
507    {
508        let hash = self.hash_key(key, execution)?;
509        let mut failure = None;
510        let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
511            equivalent(execution, key, candidate, &mut failure)
512        });
513        if let Some(reason) = failure {
514            return Err(reason);
515        }
516        let entry = match entry {
517            RawEntryMut::Occupied(entry) => {
518                // Erase examines the adjacent control groups; returning the
519                // pair moves its storage. Admit both before changing the table.
520                let work = (core::mem::size_of::<(K, V)>() as u64)
521                    .checked_mul(2)
522                    .and_then(|work| work.checked_add(2))
523                    .ok_or(MemoryError::WorkExceeded)?;
524                let removed_work = entry.key().hash_work()?;
525                execution.charge_work(work)?;
526                let (removed_key, value) = entry.remove_entry();
527                self.key_work -= removed_work;
528                // Settle metadata before caller-owned Drop can unwind.
529                drop(removed_key);
530                Some(value)
531            }
532            RawEntryMut::Vacant(_) => None,
533        };
534        Ok(entry)
535    }
536}
537
538/// A synthetic match stops hashbrown immediately on exhaustion. The caller
539/// checks failure before exposing or mutating the matched entry, so no false
540/// equality escapes and no expensive comparison runs after a refusal.
541fn equivalent<K: HashWork + Borrow<Q>, Q: HashWork + ?Sized, W: Work>(
542    memory: &W,
543    query: &Q,
544    candidate: &K,
545    failure: &mut Option<MemoryError>,
546) -> bool {
547    let cost = query.hash_work().and_then(|query| {
548        candidate.hash_work().and_then(|candidate| {
549            query
550                .checked_add(candidate)
551                .ok_or(MemoryError::WorkExceeded)
552        })
553    });
554    let admitted = cost.and_then(|cost| memory.charge_work(cost));
555    if let Err(reason) = admitted {
556        *failure = Some(reason);
557        return true;
558    }
559    query == candidate.borrow()
560}