use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::errors::WellFormedError;
pub(crate) const MAX_ENTITY_DEPTH: usize = 40;
pub(crate) const MAX_ENTITY_EXPANSIONS: usize = 10_000;
pub(crate) const MAX_EXPANDED_BYTES: usize = 10 * 1024 * 1024;
#[derive(Debug, Default)]
pub(crate) struct Budget {
expansions: AtomicUsize,
bytes: AtomicUsize,
}
impl Budget {
pub(crate) fn new() -> Arc<Self> {
Arc::new(Self::default())
}
pub(crate) fn charge(&self, entity: &str, len: usize) -> Result<(), WellFormedError> {
let expansions = self.expansions.fetch_add(1, Ordering::Relaxed) + 1;
if expansions > MAX_ENTITY_EXPANSIONS {
return Err(WellFormedError::EntityExpansionLimit {
entity: entity.to_owned(),
limit: "limit on the number of entity expansions",
});
}
let bytes = self.bytes.fetch_add(len, Ordering::Relaxed) + len;
if bytes > MAX_EXPANDED_BYTES {
return Err(WellFormedError::EntityExpansionLimit {
entity: entity.to_owned(),
limit: "limit on the total size of expanded entities",
});
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counts_expansions() {
let budget = Budget::new();
for _ in 0..MAX_ENTITY_EXPANSIONS {
assert!(budget.charge("e", 1).is_ok());
}
assert!(matches!(
budget.charge("e", 1),
Err(WellFormedError::EntityExpansionLimit { .. })
));
}
#[test]
fn counts_bytes() {
let budget = Budget::new();
assert!(budget.charge("e", MAX_EXPANDED_BYTES).is_ok());
assert!(matches!(
budget.charge("e", 1),
Err(WellFormedError::EntityExpansionLimit { .. })
));
}
#[test]
fn is_shared_between_clones() {
let budget = Budget::new();
let clone = Arc::clone(&budget);
assert!(clone.charge("e", MAX_EXPANDED_BYTES).is_ok());
assert!(matches!(
budget.charge("e", 1),
Err(WellFormedError::EntityExpansionLimit { .. })
));
}
}