Skip to main content

structured_asn1/memory/
map.rs

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