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/// A trusted key type's upper bound for hashing and equality work.
13///
14/// Implementations must cover all bytes examined by Hash and Eq. This is a
15/// contract for caller-provided code, not a value an untrusted document supplies.
16/// The bound must remain unchanged and successful while a key is stored, just
17/// as Hash and Eq must remain stable. Accounting retains this bound for rehashes.
18pub trait HashWork: Hash + Eq {
19    /// Work units for one hash/equality traversal, including constant overhead.
20    fn hash_work(&self) -> Result<u64, MemoryError>;
21}
22
23impl HashWork for usize {
24    fn hash_work(&self) -> Result<u64, MemoryError> {
25        Ok(1)
26    }
27}
28
29impl HashWork for (usize, usize, usize) {
30    fn hash_work(&self) -> Result<u64, MemoryError> {
31        Ok(3)
32    }
33}
34
35impl HashWork for &[u8] {
36    fn hash_work(&self) -> Result<u64, MemoryError> {
37        Ok(self.len() as u64 + 1)
38    }
39}
40
41impl HashWork for crate::Oid<'_> {
42    fn hash_work(&self) -> Result<u64, MemoryError> {
43        Ok(self.as_bytes().len() as u64 + 1)
44    }
45}
46
47impl HashWork for (crate::Oid<'_>, crate::Oid<'_>) {
48    fn hash_work(&self) -> Result<u64, MemoryError> {
49        self.0
50            .hash_work()?
51            .checked_add(self.1.hash_work()?)
52            .ok_or(MemoryError::WorkExceeded)
53    }
54}
55
56/// A hash map whose backing allocation and operations share one resource domain.
57///
58/// Hashbrown supplies bucket/probing/SIMD machinery. Reservations include its
59/// control bytes and alignment, not merely the initialized entries. Hashing and
60/// actual collision comparisons are admitted before examining candidate keys.
61pub struct OwnedMap<K: HashWork, V, W: Work = Memory> {
62    values: HashMap<K, V, DefaultHashBuilder, AccountAllocator<W>>,
63    execution: W,
64    key_work: u64,
65    maximum_capacity: usize,
66}
67
68impl<K: HashWork + fmt::Debug, V: fmt::Debug, W: Work> fmt::Debug for OwnedMap<K, V, W> {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        self.values.fmt(f)
71    }
72}
73
74impl<K: HashWork, V, W: Work> OwnedMap<K, V, W> {
75    pub(super) fn new(execution: W) -> Self {
76        Self {
77            values: HashMap::with_hasher_in(
78                DefaultHashBuilder::default(),
79                AccountAllocator::new(execution.clone()),
80            ),
81            execution,
82            key_work: 0,
83            maximum_capacity: 0,
84        }
85    }
86
87    /// The shared account retained until the table is deallocated.
88    #[must_use]
89    pub fn memory(&self) -> &Memory {
90        self.execution.memory()
91    }
92
93    /// Number of initialized entries.
94    #[must_use]
95    pub fn len(&self) -> usize {
96        self.values.len()
97    }
98
99    /// Whether there are no entries.
100    #[must_use]
101    pub fn is_empty(&self) -> bool {
102        self.values.is_empty()
103    }
104
105    /// Borrowed entries after admitting a full control-bucket traversal in
106    /// the current execution domain, independently of retained storage.
107    /// Sparse holes count too. Partial consumption retains the conservative
108    /// prepayment; the opaque iterator exposes no cloning/restart operation.
109    ///
110    /// # Examples
111    /// ```
112    /// # extern crate alloc;
113    /// # use alloc::sync::Arc;
114    /// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
115    /// # let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
116    /// let mut map = memory.map();
117    /// map.try_insert(1usize, 42)?;
118    /// let mut entries = map.try_iter(&memory)?;
119    /// assert_eq!(entries.next(), Some((&1, &42)));
120    /// # Ok::<(), MemoryError>(())
121    /// ```
122    pub fn try_iter<'a, E: Work>(
123        &'a self,
124        execution: &E,
125    ) -> Result<impl Iterator<Item = (&'a K, &'a V)> + 'a + use<'a, K, V, W, E>, MemoryError> {
126        execution.charge_work(self.buckets()?)?;
127        Ok(self.values.iter())
128    }
129
130    fn buckets(&self) -> Result<u64, MemoryError> {
131        // Pinned hashbrown 0.17.1 raw::bucket_mask_to_capacity: tables with
132        // four/eight buckets keep one empty slot; larger tables use 7/8 of
133        // the buckets. Maximum capacity records each table immediately after
134        // reserve, before tombstones can reduce the public capacity value.
135        match self.maximum_capacity {
136            0 => Ok(0),
137            capacity @ 1..=7 => Ok(capacity as u64 + 1),
138            capacity => (capacity as u64 / 7)
139                .checked_mul(8)
140                .ok_or(MemoryError::WorkExceeded),
141        }
142    }
143
144    fn control_work(&self) -> Result<u64, MemoryError> {
145        let buckets = self.buckets()?;
146        // The pinned engine's narrowest control group is four bytes. Wider
147        // scalar/SIMD groups perform the same fixed group operations. One unit
148        // admits one group visit; triangular probing visits each group once.
149        Ok(buckets / 4)
150    }
151
152    fn rehash_work(&self) -> Result<u64, MemoryError> {
153        // reserve(1) can double the current buckets or rehash in place. Every
154        // retained key is hashed once; each slot search may visit all groups.
155        // In-place rehash additionally scans every bucket and swaps entries.
156        let buckets = self.buckets()?;
157        let groups = buckets.checked_mul(2).ok_or(MemoryError::WorkExceeded)? / 4;
158        let moves = (core::mem::size_of::<(K, V)>() as u64)
159            .checked_mul(2)
160            .ok_or(MemoryError::WorkExceeded)?;
161        groups
162            .checked_add(moves)
163            .and_then(|per_entry| per_entry.checked_mul(self.values.len() as u64))
164            .and_then(|work| work.checked_add(self.key_work))
165            .and_then(|work| {
166                buckets
167                    .checked_mul(2)
168                    .and_then(|scan| work.checked_add(scan))
169            })
170            .ok_or(MemoryError::WorkExceeded)
171    }
172
173    fn hash_key<E: Work>(&self, key: &K, execution: &E) -> Result<u64, MemoryError> {
174        let work = key
175            .hash_work()?
176            .checked_add(self.control_work()?)
177            .ok_or(MemoryError::WorkExceeded)?;
178        execution.charge_work(work)?;
179        Ok(self.values.hasher().hash_one(key))
180    }
181
182    /// Looks up a key after admitting its complete possible probe work.
183    /// The current execution domain pays independently of stored capacity.
184    ///
185    /// # Examples
186    /// ```
187    /// # extern crate alloc;
188    /// # use alloc::sync::Arc;
189    /// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
190    /// # let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
191    /// # let mut map = memory.map();
192    /// # map.try_insert(1usize, 42)?;
193    /// assert_eq!(map.get(&1, &memory)?, Some(&42));
194    /// assert_eq!(map.get(&2, &memory)?, None);
195    /// # Ok::<(), MemoryError>(())
196    /// ```
197    pub fn get(&self, key: &K, execution: &impl Work) -> Result<Option<&V>, MemoryError> {
198        let hash = self.hash_key(key, execution)?;
199        let mut failure = None;
200        let entry = self.values.raw_entry().from_hash(hash, |candidate| {
201            equivalent(execution, key, candidate, &mut failure)
202        });
203        if let Some(reason) = failure {
204            return Err(reason);
205        }
206        Ok(entry.map(|(_, value)| value))
207    }
208
209    /// Looks up an entry for mutation without exposing table growth.
210    /// Hashing, probing and equality use the supplied execution domain.
211    ///
212    /// # Examples
213    /// ```
214    /// # extern crate alloc;
215    /// # use alloc::sync::Arc;
216    /// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
217    /// # let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
218    /// # let mut map = memory.map();
219    /// # map.try_insert(1usize, 42)?;
220    /// if let Some(value) = map.get_mut(&1, &memory)? { *value = 7; }
221    /// assert_eq!(map.get(&1, &memory)?, Some(&7));
222    /// # Ok::<(), MemoryError>(())
223    /// ```
224    pub fn get_mut(
225        &mut self,
226        key: &K,
227        execution: &impl Work,
228    ) -> Result<Option<&mut V>, MemoryError> {
229        let hash = self.hash_key(key, execution)?;
230        let mut failure = None;
231        let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
232            equivalent(execution, key, candidate, &mut failure)
233        });
234        if let Some(reason) = failure {
235            return Err(reason);
236        }
237        Ok(match entry {
238            RawEntryMut::Occupied(entry) => Some(entry.into_mut()),
239            RawEntryMut::Vacant(_) => None,
240        })
241    }
242
243    /// Inserts a value fallibly, or replaces the value for an existing key.
244    /// This storage mutation runs in the collection's original resource
245    /// domain, including hashing, rehashing, allocation and copying work.
246    /// To mutate under a different domain, build a new collection there from
247    /// explicitly admitted borrowed entries; retained capacity is not rebound.
248    pub fn try_insert(&mut self, key: K, value: V) -> Result<Option<V>, MemoryError> {
249        let hash = self.hash_key(&key, &self.execution)?;
250        let mut failure = None;
251        let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
252            equivalent(&self.execution, &key, candidate, &mut failure)
253        });
254        if let Some(reason) = failure {
255            return Err(reason);
256        }
257        if let RawEntryMut::Occupied(entry) = entry {
258            let moves = (core::mem::size_of::<V>() as u64)
259                .checked_mul(2)
260                .ok_or(MemoryError::WorkExceeded)?;
261            self.execution.charge_work(moves)?;
262            return Ok(Some(core::mem::replace(entry.into_mut(), value)));
263        }
264        let key_work = self
265            .key_work
266            .checked_add(key.hash_work()?)
267            .ok_or(MemoryError::WorkExceeded)?;
268        if self.values.len() == self.values.capacity() {
269            // Include collision-dependent slot searches, not just key hashes.
270            self.execution.charge_work(self.rehash_work()?)?;
271            self.values.allocator().clear_failure();
272            self.values.try_reserve(1).map_err(|error| match error {
273                TryReserveError::CapacityOverflow => MemoryError::CapacityOverflow,
274                TryReserveError::AllocError { .. } => self.values.allocator().failure(),
275            })?;
276            self.maximum_capacity = self.maximum_capacity.max(self.values.capacity());
277        }
278        // Reuse the already computed hash after reserve. No key equality is
279        // needed: the first lookup established absence and resize preserves keys.
280        // raw_entry searches for a match; insert_hashed_nocheck subsequently
281        // searches for an insertion slot. Admit both control traversals.
282        self.execution.charge_work(
283            self.control_work()?
284                .checked_mul(2)
285                .and_then(|work| work.checked_add(core::mem::size_of::<(K, V)>() as u64))
286                .ok_or(MemoryError::WorkExceeded)?,
287        )?;
288        let RawEntryMut::Vacant(entry) = self.values.raw_entry_mut().from_hash(hash, |_| false)
289        else {
290            unreachable!("a predicate that always refuses cannot find an entry");
291        };
292        entry.insert_hashed_nocheck(hash, key, value);
293        self.key_work = key_work;
294        Ok(None)
295    }
296
297    /// Removes an entry, retaining table capacity and its storage charge.
298    /// The supplied execution domain pays for lookup and removal before mutation.
299    ///
300    /// # Examples
301    /// ```
302    /// # extern crate alloc;
303    /// # use alloc::sync::Arc;
304    /// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
305    /// # let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
306    /// # let mut map = memory.map();
307    /// # map.try_insert(1usize, 42)?;
308    /// let capacity_charge = memory.used();
309    /// assert_eq!(map.remove(&1, &memory)?, Some(42));
310    /// assert_eq!(memory.used(), capacity_charge);
311    /// # Ok::<(), MemoryError>(())
312    /// ```
313    pub fn remove(&mut self, key: &K, execution: &impl Work) -> Result<Option<V>, MemoryError> {
314        let hash = self.hash_key(key, execution)?;
315        let mut failure = None;
316        let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
317            equivalent(execution, key, candidate, &mut failure)
318        });
319        if let Some(reason) = failure {
320            return Err(reason);
321        }
322        let entry = match entry {
323            RawEntryMut::Occupied(entry) => {
324                // Erase examines the adjacent control groups; returning the
325                // pair moves its storage. Admit both before changing the table.
326                let work = (core::mem::size_of::<(K, V)>() as u64)
327                    .checked_mul(2)
328                    .and_then(|work| work.checked_add(2))
329                    .ok_or(MemoryError::WorkExceeded)?;
330                let removed_work = entry.key().hash_work()?;
331                execution.charge_work(work)?;
332                let (_, value) = entry.remove_entry();
333                self.key_work -= removed_work;
334                Some(value)
335            }
336            RawEntryMut::Vacant(_) => None,
337        };
338        Ok(entry)
339    }
340}
341
342/// A synthetic match stops hashbrown immediately on exhaustion. The caller
343/// checks failure before exposing or mutating the matched entry, so no false
344/// equality escapes and no expensive comparison runs after a refusal.
345fn equivalent<K: HashWork, W: Work>(
346    memory: &W,
347    query: &K,
348    candidate: &K,
349    failure: &mut Option<MemoryError>,
350) -> bool {
351    let cost = query.hash_work().and_then(|query| {
352        candidate.hash_work().and_then(|candidate| {
353            query
354                .checked_add(candidate)
355                .ok_or(MemoryError::WorkExceeded)
356        })
357    });
358    let admitted = cost.and_then(|cost| memory.charge_work(cost));
359    if let Err(reason) = admitted {
360        *failure = Some(reason);
361        return true;
362    }
363    query == candidate
364}