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};
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> {
62    values: HashMap<K, V, DefaultHashBuilder, AccountAllocator>,
63    memory: Memory,
64    key_work: u64,
65    maximum_capacity: usize,
66}
67
68impl<K: HashWork + fmt::Debug, V: fmt::Debug> fmt::Debug for OwnedMap<K, V> {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        self.values.fmt(f)
71    }
72}
73
74impl<K: HashWork, V> OwnedMap<K, V> {
75    pub(super) fn new(memory: Memory) -> Self {
76        Self {
77            values: HashMap::with_hasher_in(
78                DefaultHashBuilder::default(),
79                AccountAllocator::new(memory.clone()),
80            ),
81            memory,
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.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>(
123        &'a self,
124        execution: &Memory,
125    ) -> Result<impl Iterator<Item = (&'a K, &'a V)> + use<'a, K, V>, 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.16.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(&self, key: &K, execution: &Memory) -> 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: &Memory) -> 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(&mut self, key: &K, execution: &Memory) -> Result<Option<&mut V>, MemoryError> {
225        let hash = self.hash_key(key, execution)?;
226        let mut failure = None;
227        let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
228            equivalent(execution, key, candidate, &mut failure)
229        });
230        if let Some(reason) = failure {
231            return Err(reason);
232        }
233        Ok(match entry {
234            RawEntryMut::Occupied(entry) => Some(entry.into_mut()),
235            RawEntryMut::Vacant(_) => None,
236        })
237    }
238
239    /// Inserts a value fallibly, or replaces the value for an existing key.
240    /// This storage mutation runs in the collection's original resource
241    /// domain, including hashing, rehashing, allocation and copying work.
242    /// To mutate under a different domain, build a new collection there from
243    /// explicitly admitted borrowed entries; retained capacity is not rebound.
244    pub fn try_insert(&mut self, key: K, value: V) -> Result<Option<V>, MemoryError> {
245        let hash = self.hash_key(&key, &self.memory)?;
246        let mut failure = None;
247        let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
248            equivalent(&self.memory, &key, candidate, &mut failure)
249        });
250        if let Some(reason) = failure {
251            return Err(reason);
252        }
253        if let RawEntryMut::Occupied(entry) = entry {
254            return Ok(Some(core::mem::replace(entry.into_mut(), value)));
255        }
256        let key_work = self
257            .key_work
258            .checked_add(key.hash_work()?)
259            .ok_or(MemoryError::WorkExceeded)?;
260        if self.values.len() == self.values.capacity() {
261            // Include collision-dependent slot searches, not just key hashes.
262            self.memory().charge_work(self.rehash_work()?)?;
263            self.values.allocator().clear_failure();
264            self.values.try_reserve(1).map_err(|error| match error {
265                TryReserveError::CapacityOverflow => MemoryError::CapacityOverflow,
266                TryReserveError::AllocError { .. } => self.values.allocator().failure(),
267            })?;
268            self.maximum_capacity = self.maximum_capacity.max(self.values.capacity());
269        }
270        // Reuse the already computed hash after reserve. No key equality is
271        // needed: the first lookup established absence and resize preserves keys.
272        // raw_entry searches for a match; insert_hashed_nocheck subsequently
273        // searches for an insertion slot. Admit both control traversals.
274        self.memory.charge_work(
275            self.control_work()?
276                .checked_mul(2)
277                .ok_or(MemoryError::WorkExceeded)?,
278        )?;
279        let RawEntryMut::Vacant(entry) = self.values.raw_entry_mut().from_hash(hash, |_| false)
280        else {
281            unreachable!("a predicate that always refuses cannot find an entry");
282        };
283        entry.insert_hashed_nocheck(hash, key, value);
284        self.key_work = key_work;
285        Ok(None)
286    }
287
288    /// Removes an entry, retaining table capacity and its storage charge.
289    /// The supplied execution domain pays for lookup and removal before mutation.
290    ///
291    /// # Examples
292    /// ```
293    /// # extern crate alloc;
294    /// # use alloc::sync::Arc;
295    /// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
296    /// # let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
297    /// # let mut map = memory.map();
298    /// # map.try_insert(1usize, 42)?;
299    /// let capacity_charge = memory.used();
300    /// assert_eq!(map.remove(&1, &memory)?, Some(42));
301    /// assert_eq!(memory.used(), capacity_charge);
302    /// # Ok::<(), MemoryError>(())
303    /// ```
304    pub fn remove(&mut self, key: &K, execution: &Memory) -> Result<Option<V>, MemoryError> {
305        let hash = self.hash_key(key, execution)?;
306        let mut failure = None;
307        let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
308            equivalent(execution, key, candidate, &mut failure)
309        });
310        if let Some(reason) = failure {
311            return Err(reason);
312        }
313        let entry = match entry {
314            RawEntryMut::Occupied(entry) => {
315                // Erase examines the adjacent control groups; returning the
316                // pair moves its storage. Admit both before changing the table.
317                let work = (core::mem::size_of::<(K, V)>() as u64)
318                    .checked_mul(2)
319                    .and_then(|work| work.checked_add(2))
320                    .ok_or(MemoryError::WorkExceeded)?;
321                let removed_work = entry.key().hash_work()?;
322                execution.charge_work(work)?;
323                let (_, value) = entry.remove_entry();
324                self.key_work -= removed_work;
325                Some(value)
326            }
327            RawEntryMut::Vacant(_) => None,
328        };
329        Ok(entry)
330    }
331}
332
333/// A synthetic match stops hashbrown immediately on exhaustion. The caller
334/// checks failure before exposing or mutating the matched entry, so no false
335/// equality escapes and no expensive comparison runs after a refusal.
336fn equivalent<K: HashWork>(
337    memory: &Memory,
338    query: &K,
339    candidate: &K,
340    failure: &mut Option<MemoryError>,
341) -> bool {
342    let cost = query.hash_work().and_then(|query| {
343        candidate.hash_work().and_then(|candidate| {
344            query
345                .checked_add(candidate)
346                .ok_or(MemoryError::WorkExceeded)
347        })
348    });
349    let admitted = cost.and_then(|cost| memory.charge_work(cost));
350    if let Err(reason) = admitted {
351        *failure = Some(reason);
352        return true;
353    }
354    query == candidate
355}