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