use crate::DecodeError;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DecodeLimits {
pub max_input_bytes: usize,
pub max_list_items: usize,
pub max_nesting_depth: usize,
pub max_total_allocation: usize,
pub max_proof_nodes: usize,
pub max_total_items: usize,
}
impl DecodeLimits {
#[cfg(any(test, feature = "testing"))]
pub const TEST_FIXTURE: Self = Self {
max_input_bytes: 1 << 20,
max_list_items: 4096,
max_nesting_depth: 64,
max_total_allocation: 1 << 20,
max_proof_nodes: 1024,
max_total_items: 8192,
};
#[doc(alias = "DEPLOYMENT_TEMPLATE")]
pub const DEPLOYMENT_STARTING_POINT: Self = Self {
max_input_bytes: 2 << 20,
max_list_items: 16_384,
max_nesting_depth: 64,
max_total_allocation: 4 << 20,
max_proof_nodes: 4096,
max_total_items: 65_536,
};
pub fn check_input_len(self, len: usize) -> Result<(), DecodeError> {
if len > self.max_input_bytes {
return Err(DecodeError::InputTooLarge);
}
Ok(())
}
pub fn check_list_count(self, count: usize) -> Result<(), DecodeError> {
if count > self.max_list_items {
return Err(DecodeError::ListTooLong);
}
Ok(())
}
pub fn check_nesting_depth(self, depth: usize) -> Result<(), DecodeError> {
if depth > self.max_nesting_depth {
return Err(DecodeError::NestingTooDeep);
}
Ok(())
}
pub fn check_single_allocation_limit(self, size: usize) -> Result<(), DecodeError> {
if size > self.max_total_allocation {
return Err(DecodeError::AllocationExceeded);
}
Ok(())
}
pub fn check_proof_node_count(self, count: usize) -> Result<(), DecodeError> {
if count > self.max_proof_nodes {
return Err(DecodeError::ProofTooLarge);
}
Ok(())
}
pub fn check_item_count(self, count: usize) -> Result<(), DecodeError> {
if count > self.max_total_items {
return Err(DecodeError::ItemCountExceeded);
}
Ok(())
}
#[must_use]
pub const fn accumulator(self) -> DecodeAccumulator {
DecodeAccumulator {
limits: self,
total_allocated: 0,
total_items: 0,
proof_nodes: 0,
}
}
pub fn validate_deployment_policy(self) -> Result<(), DecodeError> {
if self == Self::DEPLOYMENT_STARTING_POINT {
return Err(DecodeError::UnreviewedDeploymentPolicy);
}
Ok(())
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct DecodeAccumulator {
limits: DecodeLimits,
total_allocated: usize,
total_items: usize,
proof_nodes: usize,
}
impl DecodeAccumulator {
#[must_use]
pub const fn limits(&self) -> DecodeLimits {
self.limits
}
#[must_use]
pub const fn total_allocated(&self) -> usize {
self.total_allocated
}
#[must_use]
pub const fn total_items(&self) -> usize {
self.total_items
}
#[must_use]
pub const fn proof_nodes(&self) -> usize {
self.proof_nodes
}
pub fn check_input_len(&self, len: usize) -> Result<(), DecodeError> {
self.limits.check_input_len(len)
}
pub fn check_list_count(&self, count: usize) -> Result<(), DecodeError> {
self.limits.check_list_count(count)
}
pub fn check_nesting_depth(&self, depth: usize) -> Result<(), DecodeError> {
self.limits.check_nesting_depth(depth)
}
pub fn check_allocation(&mut self, size: usize) -> Result<(), DecodeError> {
let new_total = self
.total_allocated
.checked_add(size)
.ok_or(DecodeError::AllocationExceeded)?;
if new_total > self.limits.max_total_allocation {
return Err(DecodeError::AllocationExceeded);
}
self.total_allocated = new_total;
Ok(())
}
pub fn account_items(&mut self, count: usize) -> Result<(), DecodeError> {
let new_total = self
.total_items
.checked_add(count)
.ok_or(DecodeError::ItemCountExceeded)?;
if new_total > self.limits.max_total_items {
return Err(DecodeError::ItemCountExceeded);
}
self.total_items = new_total;
Ok(())
}
pub fn account_proof_nodes(&mut self, count: usize) -> Result<(), DecodeError> {
let new_total = self
.proof_nodes
.checked_add(count)
.ok_or(DecodeError::ProofTooLarge)?;
if new_total > self.limits.max_proof_nodes {
return Err(DecodeError::ProofTooLarge);
}
self.proof_nodes = new_total;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_oversized_input() {
let limits = DecodeLimits {
max_input_bytes: 2,
..DecodeLimits::TEST_FIXTURE
};
assert_eq!(limits.check_input_len(3), Err(DecodeError::InputTooLarge));
}
#[test]
fn rejects_oversized_list() {
let limits = DecodeLimits {
max_list_items: 2,
..DecodeLimits::TEST_FIXTURE
};
assert_eq!(limits.check_list_count(3), Err(DecodeError::ListTooLong));
}
#[test]
fn rejects_excessive_nesting_depth() {
let limits = DecodeLimits {
max_nesting_depth: 2,
..DecodeLimits::TEST_FIXTURE
};
assert_eq!(
limits.check_nesting_depth(3),
Err(DecodeError::NestingTooDeep)
);
}
#[test]
fn rejects_excessive_allocation() {
let limits = DecodeLimits {
max_total_allocation: 2,
..DecodeLimits::TEST_FIXTURE
};
assert_eq!(
limits.check_single_allocation_limit(3),
Err(DecodeError::AllocationExceeded)
);
}
#[test]
fn rejects_excessive_proof_nodes() {
let limits = DecodeLimits {
max_proof_nodes: 2,
..DecodeLimits::TEST_FIXTURE
};
assert_eq!(
limits.check_proof_node_count(3),
Err(DecodeError::ProofTooLarge)
);
}
#[test]
fn rejects_excessive_total_items() {
let limits = DecodeLimits {
max_total_items: 2,
..DecodeLimits::TEST_FIXTURE
};
assert_eq!(
limits.check_item_count(3),
Err(DecodeError::ItemCountExceeded)
);
}
#[test]
fn fixture_and_deployment_starting_point_limits_are_distinct() {
let production = DecodeLimits::DEPLOYMENT_STARTING_POINT;
let fixture = DecodeLimits::TEST_FIXTURE;
assert!(production.max_input_bytes > fixture.max_input_bytes);
assert!(production.max_total_allocation > fixture.max_total_allocation);
assert!(production.max_proof_nodes > fixture.max_proof_nodes);
assert!(production.max_total_items > fixture.max_total_items);
}
#[test]
fn accumulator_rejects_cumulative_allocation_over_budget() {
let limits = DecodeLimits {
max_total_allocation: 4,
..DecodeLimits::TEST_FIXTURE
};
let mut accumulator = limits.accumulator();
assert_eq!(accumulator.check_allocation(3), Ok(()));
assert_eq!(accumulator.total_allocated(), 3);
assert_eq!(
accumulator.check_allocation(2),
Err(DecodeError::AllocationExceeded)
);
assert_eq!(accumulator.total_allocated(), 3);
}
#[test]
fn accumulator_rejects_allocation_counter_overflow() {
let limits = DecodeLimits {
max_total_allocation: usize::MAX,
..DecodeLimits::TEST_FIXTURE
};
let mut accumulator = limits.accumulator();
assert_eq!(accumulator.check_allocation(usize::MAX), Ok(()));
assert_eq!(
accumulator.check_allocation(1),
Err(DecodeError::AllocationExceeded)
);
}
#[test]
fn accumulator_rejects_cumulative_items_over_budget() {
let limits = DecodeLimits {
max_total_items: 4,
..DecodeLimits::TEST_FIXTURE
};
let mut accumulator = limits.accumulator();
assert_eq!(accumulator.account_items(3), Ok(()));
assert_eq!(accumulator.total_items(), 3);
assert_eq!(
accumulator.account_items(2),
Err(DecodeError::ItemCountExceeded)
);
assert_eq!(accumulator.total_items(), 3);
}
#[test]
fn accumulator_rejects_cumulative_proof_nodes_over_budget() {
let limits = DecodeLimits {
max_proof_nodes: 4,
..DecodeLimits::TEST_FIXTURE
};
let mut accumulator = limits.accumulator();
assert_eq!(accumulator.account_proof_nodes(3), Ok(()));
assert_eq!(accumulator.proof_nodes(), 3);
assert_eq!(
accumulator.account_proof_nodes(2),
Err(DecodeError::ProofTooLarge)
);
assert_eq!(accumulator.proof_nodes(), 3);
}
#[test]
fn deployment_starting_point_must_be_reviewed_before_use() {
assert_eq!(
DecodeLimits::DEPLOYMENT_STARTING_POINT.validate_deployment_policy(),
Err(DecodeError::UnreviewedDeploymentPolicy)
);
let reviewed = DecodeLimits {
max_input_bytes: DecodeLimits::DEPLOYMENT_STARTING_POINT.max_input_bytes / 2,
..DecodeLimits::DEPLOYMENT_STARTING_POINT
};
assert_eq!(reviewed.validate_deployment_policy(), Ok(()));
}
}