use std::{borrow::Cow, mem::size_of};
use crate::{Budget, EvaluationError, Location, QueryError, QueryErrorCategory};
pub fn expand_fragments<'a, P, C: ?Sized, E>(
parts: impl IntoIterator<Item = P>,
context: &mut C,
budget: &mut Budget,
location: Location,
mut resolve: impl FnMut(P, &mut C, &mut Budget) -> Result<Cow<'a, str>, EvaluationError<E>>,
) -> Result<String, EvaluationError<E>> {
budget.within_evaluation(0, location.clone(), |budget| {
budget.charge_results(1, location.clone())?;
let mut output = String::new();
for part in parts {
budget.charge(1, location.clone())?;
let text = resolve(part, context, budget)?;
output
.len()
.checked_add(text.len())
.filter(|bytes| *bytes <= isize::MAX as usize)
.ok_or_else(|| storage_error(&location, "fragment storage limit exceeded"))?;
budget.charge(text.len(), location.clone())?;
budget.charge_results(text.len(), location.clone())?;
output.push_str(&text);
}
Ok(output)
})
}
pub fn construct_string_entries<I, C: ?Sized, V, E>(
entries: impl IntoIterator<Item = I>,
context: &mut C,
budget: &mut Budget,
location: Location,
key: impl FnMut(&I, &mut C, &mut Budget) -> Result<String, EvaluationError<E>>,
value: impl FnMut(I, &mut C, &mut Budget) -> Result<V, EvaluationError<E>>,
duplicate: impl FnMut(&str, &mut C, &mut Budget) -> Result<QueryError, EvaluationError<E>>,
) -> Result<Vec<(String, V)>, EvaluationError<E>> {
budget.within_evaluation(1, location.clone(), |budget| {
construct_entries(
entries,
context,
budget,
&location,
key,
value,
duplicate,
EntryAccounting::Computed,
)
})
}
pub(crate) enum EntryAccounting {
Computed,
NativeMap,
}
impl EntryAccounting {
fn charge(
&self,
budget: &mut Budget,
units: usize,
location: &Location,
) -> Result<(), QueryError> {
match self {
Self::Computed => budget.charge(units, location.clone()),
Self::NativeMap => budget.charge_program_work(units, location.clone()),
}
}
pub(crate) fn entries<V>(
&self,
budget: &mut Budget,
location: &Location,
) -> Result<Vec<(String, V)>, QueryError> {
self.charge(budget, 1, location)?;
budget.charge_results(1, location.clone())?;
Ok(Vec::new())
}
pub(crate) fn contains<V>(
&self,
entries: &[(String, V)],
key: &str,
budget: &mut Budget,
location: &Location,
) -> Result<bool, QueryError> {
let count = entries
.len()
.checked_add(1)
.ok_or_else(|| self.storage_error(location))?;
count
.checked_mul(size_of::<(String, V)>())
.filter(|bytes| *bytes <= isize::MAX as usize)
.ok_or_else(|| self.storage_error(location))?;
let inspected = key
.len()
.checked_add(1)
.and_then(|per_key| per_key.checked_mul(count))
.ok_or_else(|| self.storage_error(location))?;
self.charge(budget, inspected, location)?;
Ok(entries.iter().any(|(previous, _)| previous == key))
}
pub(crate) fn append<V>(
&self,
entries: &mut Vec<(String, V)>,
key: String,
value: V,
budget: &mut Budget,
location: &Location,
) -> Result<(), QueryError> {
let count = entries
.len()
.checked_add(1)
.ok_or_else(|| self.storage_error(location))?;
budget.check_items(count, location.clone())?;
self.charge(budget, 1, location)?;
budget.charge_results(1, location.clone())?;
entries.push((key, value));
Ok(())
}
fn storage_error(&self, location: &Location) -> QueryError {
storage_error(
location,
match self {
Self::Computed => "object construction storage limit exceeded",
Self::NativeMap => "native map storage limit exceeded",
},
)
}
}
#[expect(
clippy::too_many_arguments,
reason = "The shared loop retains separate key, value and diagnostic callbacks and the existing native-map accounting policy."
)]
pub(crate) fn construct_entries<I, C: ?Sized, V, E>(
entries: impl IntoIterator<Item = I>,
context: &mut C,
budget: &mut Budget,
location: &Location,
mut key: impl FnMut(&I, &mut C, &mut Budget) -> Result<String, EvaluationError<E>>,
mut value: impl FnMut(I, &mut C, &mut Budget) -> Result<V, EvaluationError<E>>,
mut duplicate: impl FnMut(&str, &mut C, &mut Budget) -> Result<QueryError, EvaluationError<E>>,
accounting: EntryAccounting,
) -> Result<Vec<(String, V)>, EvaluationError<E>> {
let mut result = accounting.entries(budget, location)?;
for item in entries {
accounting.charge(budget, 1, location)?;
let key = key(&item, context, budget)?;
if accounting.contains(&result, &key, budget, location)? {
return Err(duplicate(&key, context, budget)?.into());
}
let mapped = value(item, context, budget)?;
accounting.append(&mut result, key, mapped, budget, location)?;
}
Ok(result)
}
fn storage_error(location: &Location, message: &str) -> QueryError {
QueryError {
location: location.clone(),
message: message.into(),
category: QueryErrorCategory::ResultLimit,
}
}