use super::{u64_from_index, DecodeContext, ScopedReservation};
use crate::CodecError;
use std::collections::HashMap;
use std::hash::Hash;
impl DecodeContext<'_> {
pub fn unique_index<K: Eq + Hash, V>(
&self,
entries: impl IntoIterator<Item = (K, V)>,
key_work: impl Fn(&K) -> Result<u64, CodecError>,
operation: &'static str,
) -> Result<(HashMap<K, Option<V>>, ScopedReservation<'_>), CodecError> {
let mut table = HashMap::<K, Option<V>>::new();
let mut storage = self.reserve_scoped(0, operation)?;
for (key, value) in entries {
self.charge_work(1, operation)?;
self.charge_work(key_work(&key)?, operation)?;
if let Some(previous) = table.get_mut(&key) {
*previous = None;
continue;
}
self.charge_collection_items(1, operation)?;
if table.len() == table.capacity() {
self.charge_work(u64_from_index(table.capacity()), operation)?;
for stored in table.keys() {
self.charge_work(key_work(stored)?, operation)?;
}
let required = table
.len()
.checked_add(1)
.and_then(|count| count.checked_mul(4))
.and_then(|count| {
count.checked_mul(
std::mem::size_of::<(K, Option<V>)>()
.max(1)
.checked_add(32)?,
)
})
.ok_or_else(|| {
CodecError::from(self.budget.scoped_size_overflow_limit(operation))
})?;
storage.grow(u64_from_index(required))?;
table.try_reserve(1).map_err(|_| {
self.budget
.scoped_allocation_failed(u64_from_index(required), operation)
})?;
}
self.charge_work(key_work(&key)?, operation)?;
let _ = table.insert(key, Some(value));
}
self.charge_work(u64_from_index(table.capacity()), operation)?;
table.retain(|_, value| value.is_some());
Ok((table, storage))
}
}
#[cfg(test)]
mod tests {
use crate::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use crate::CodecError;
#[test]
fn unique_index_removes_all_duplicate_occurrences_in_one_table() {
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_collection_items = 2;
policy.limits.max_materialized_bytes =
super::u64_from_index(4 * (std::mem::size_of::<(u32, Option<i32>)>() + 32));
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("context");
let (table, storage) = ctx
.unique_index(
[(1_u32, 2), (1, 3), (1, 4), (2, 5)],
|_| Ok(4),
"unique test",
)
.expect("two slots");
assert_eq!(table.len(), 1);
assert_eq!(table.get(&2), Some(&Some(5)));
assert!(!table.contains_key(&1));
drop((table, storage));
ctx.reserve_scoped(policy.limits.max_materialized_bytes, "released table")
.expect("storage released");
}
#[test]
fn unique_index_one_slot_keeps_one_record() {
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_collection_items = 1;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("context");
let (table, _storage) = ctx
.unique_index([(7_u32, 7)], |_| Ok(4), "unique test")
.expect("one table slot");
assert_eq!(table.get(&7), Some(&Some(7)));
}
#[test]
fn unique_index_propagates_slot_workspace_and_key_work_refusals() {
for dimension in [
ResourceDimension::CollectionItems,
ResourceDimension::MaterializedBytes,
ResourceDimension::WorkUnits,
] {
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
match dimension {
ResourceDimension::CollectionItems => policy.limits.max_collection_items = 0,
ResourceDimension::MaterializedBytes => policy.limits.max_materialized_bytes = 0,
ResourceDimension::WorkUnits => policy.limits.max_work_units = 3,
_ => panic!("test dimension"),
}
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("context");
assert!(
matches!(ctx.unique_index([("long key", 1)], |key| Ok(super::u64_from_index(key.len())), "unique test"),
Err(CodecError::ResourceLimit(limit)) if limit.dimension == dimension)
);
}
}
}