use std::cell::{Cell, RefCell};
use std::rc::Rc;
use oneyaml_core::{Limits, Span, code};
use crate::de::scalar::VariantMemo;
use crate::error::Error;
pub(crate) struct ReadShared {
pub(crate) meter: ByteMeter,
pub(crate) variants: RefCell<VariantMemo>,
}
pub(crate) type SharedRead = Rc<ReadShared>;
impl ReadShared {
pub(crate) fn new(limits: &Limits) -> SharedRead {
Rc::new(Self {
meter: ByteMeter::new(limits),
variants: RefCell::default(),
})
}
}
#[derive(Debug)]
pub(crate) struct ByteMeter {
limits: Limits,
spent: Cell<usize>,
input: Cell<usize>,
}
impl ByteMeter {
pub(crate) fn new(limits: &Limits) -> Self {
Self {
limits: limits.clone(),
spent: Cell::new(0),
input: Cell::new(0),
}
}
pub(crate) fn reset(&self) {
self.spent.set(0);
self.input.set(0);
}
pub(crate) fn read_to(&self, bytes: usize) {
if bytes > self.input.get() {
self.input.set(bytes);
}
}
fn budget(&self) -> Option<usize> {
self.limits.get_alias_byte_budget(self.input.get())
}
pub(crate) fn check(&self, bytes: usize, span: Option<Span>) -> Result<(), Error> {
match self.budget() {
Some(budget) if self.spent.get().saturating_add(bytes) > budget => {
Err(alias_byte_budget_exceeded(budget, self.input.get(), span))
}
_ => Ok(()),
}
}
pub(crate) fn charge(&self, bytes: usize, span: Option<Span>) -> Result<(), Error> {
if bytes == 0 {
return Ok(());
}
self.check(bytes, span)?;
self.spent.set(self.spent.get().saturating_add(bytes));
Ok(())
}
}
pub(crate) fn alias_byte_budget_exceeded(budget: usize, input: usize, span: Option<Span>) -> Error {
Error::new(
code::ALIAS_BYTE_BUDGET_EXCEEDED,
format!(
"expansion (alias copies and `%TAG` prefixes repeated per use) \
materialises more than {budget} bytes \
(after {input} bytes of the document)"
),
span,
)
.with_help(format!(
"raise Limits::alias_byte_budget (currently {budget} for this document), \
or use Limits::unlimited() for fully trusted input"
))
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
use super::*;
#[test]
fn charges_up_to_the_budget_and_scales_with_the_input() {
let m = ByteMeter::new(&Limits::unlimited().alias_byte_budget_ratio(2, 10));
m.charge(10, None).unwrap();
let e = m.charge(1, None).unwrap_err();
assert_eq!(e.code(), code::ALIAS_BYTE_BUDGET_EXCEEDED);
assert!(
e.diagnostic()
.help
.as_deref()
.unwrap()
.contains("alias_byte_budget")
);
m.read_to(6);
m.read_to(3);
m.check(2, None).unwrap();
m.charge(2, None).unwrap();
assert!(m.check(1, None).is_err());
m.reset();
m.charge(10, None).unwrap();
let off = ByteMeter::new(&Limits::unlimited());
off.charge(usize::MAX, None).unwrap();
off.charge(usize::MAX, None).unwrap();
off.charge(0, None).unwrap();
}
}