#![no_std]
#![forbid(unsafe_code)]
#[cfg(feature = "std")]
extern crate std;
use core::{cmp::Ordering, fmt};
#[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,
}
impl DecodeLimits {
pub const TEST_FIXTURE: Self = Self {
max_input_bytes: 1 << 20,
max_list_items: 4096,
max_nesting_depth: 64,
max_total_allocation: 1 << 20,
};
pub const PRODUCTION_RECOMMENDED: Self = Self {
max_input_bytes: 2 << 20,
max_list_items: 16_384,
max_nesting_depth: 64,
max_total_allocation: 4 << 20,
};
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(())
}
#[must_use]
pub const fn accumulator(self) -> DecodeAccumulator {
DecodeAccumulator {
limits: self,
total_allocated: 0,
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct DecodeAccumulator {
limits: DecodeLimits,
total_allocated: 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
}
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(())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecodeError {
InputTooLarge,
TrailingBytes,
DecoderOverread,
Malformed,
ListTooLong,
NestingTooDeep,
AllocationExceeded,
}
impl DecodeError {
#[must_use]
pub const fn code(self) -> &'static str {
match self {
Self::InputTooLarge => "ETH_CODEC_INPUT_TOO_LARGE",
Self::TrailingBytes => "ETH_CODEC_TRAILING_BYTES",
Self::DecoderOverread => "ETH_CODEC_DECODER_OVERREAD",
Self::Malformed => "ETH_CODEC_MALFORMED",
Self::ListTooLong => "ETH_CODEC_LIST_TOO_LONG",
Self::NestingTooDeep => "ETH_CODEC_NESTING_TOO_DEEP",
Self::AllocationExceeded => "ETH_CODEC_ALLOCATION_EXCEEDED",
}
}
#[must_use]
pub const fn message(self) -> &'static str {
match self {
Self::InputTooLarge => "input exceeds the active decode byte limit",
Self::TrailingBytes => "decoded value did not consume the full input",
Self::DecoderOverread => "decoder consumed more bytes than were available",
Self::Malformed => "input is malformed for the selected codec",
Self::ListTooLong => "decoded list exceeds the active item limit",
Self::NestingTooDeep => "decoded structure exceeds the active nesting limit",
Self::AllocationExceeded => "decoder exceeded the active allocation limit",
}
}
#[must_use]
pub const fn category(self) -> DecodeErrorCategory {
match self {
Self::InputTooLarge
| Self::ListTooLong
| Self::NestingTooDeep
| Self::AllocationExceeded => DecodeErrorCategory::ResourceExhaustion,
Self::TrailingBytes | Self::DecoderOverread | Self::Malformed => {
DecodeErrorCategory::MalformedInput
}
}
}
#[must_use]
pub const fn resource(self) -> Option<ResourceError> {
match self {
Self::InputTooLarge => Some(ResourceError::InputBytes),
Self::ListTooLong => Some(ResourceError::ListItems),
Self::NestingTooDeep => Some(ResourceError::NestingDepth),
Self::AllocationExceeded => Some(ResourceError::AllocationBytes),
Self::TrailingBytes | Self::DecoderOverread | Self::Malformed => None,
}
}
}
impl fmt::Display for DecodeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.message())
}
}
#[cfg(feature = "std")]
impl std::error::Error for DecodeError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecodeErrorCategory {
MalformedInput,
ResourceExhaustion,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ResourceError {
InputBytes,
ListItems,
NestingDepth,
AllocationBytes,
}
impl ResourceError {
#[must_use]
pub const fn code(self) -> &'static str {
match self {
Self::InputBytes => "ETH_RESOURCE_INPUT_BYTES",
Self::ListItems => "ETH_RESOURCE_LIST_ITEMS",
Self::NestingDepth => "ETH_RESOURCE_NESTING_DEPTH",
Self::AllocationBytes => "ETH_RESOURCE_ALLOCATION_BYTES",
}
}
#[must_use]
pub const fn message(self) -> &'static str {
match self {
Self::InputBytes => "input byte budget exceeded",
Self::ListItems => "list item budget exceeded",
Self::NestingDepth => "nesting depth budget exceeded",
Self::AllocationBytes => "allocation byte budget exceeded",
}
}
}
impl fmt::Display for ResourceError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.message())
}
}
#[cfg(feature = "std")]
impl std::error::Error for ResourceError {}
pub fn require_exact_consumption(consumed: usize, input_len: usize) -> Result<(), DecodeError> {
match consumed.cmp(&input_len) {
Ordering::Equal => Ok(()),
Ordering::Less => Err(DecodeError::TrailingBytes),
Ordering::Greater => Err(DecodeError::DecoderOverread),
}
}
#[cfg(test)]
mod tests {
use super::*;
extern crate std;
use std::string::ToString;
#[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 fixture_and_production_limits_are_distinct() {
let production = DecodeLimits::PRODUCTION_RECOMMENDED;
let fixture = DecodeLimits::TEST_FIXTURE;
assert!(production.max_input_bytes > fixture.max_input_bytes);
assert!(production.max_total_allocation > fixture.max_total_allocation);
}
#[test]
fn decode_errors_have_stable_codes_and_messages() {
assert_eq!(DecodeError::Malformed.code(), "ETH_CODEC_MALFORMED");
assert_eq!(
DecodeError::Malformed.message(),
"input is malformed for the selected codec"
);
assert_eq!(
DecodeError::Malformed.category(),
DecodeErrorCategory::MalformedInput
);
assert_eq!(
DecodeError::Malformed.to_string(),
"input is malformed for the selected codec"
);
}
#[test]
fn resource_errors_are_classified_without_payloads() {
let error = DecodeError::AllocationExceeded;
assert_eq!(error.category(), DecodeErrorCategory::ResourceExhaustion);
assert_eq!(error.resource(), Some(ResourceError::AllocationBytes));
assert_eq!(
ResourceError::AllocationBytes.code(),
"ETH_RESOURCE_ALLOCATION_BYTES"
);
assert_eq!(
ResourceError::AllocationBytes.to_string(),
"allocation byte budget exceeded"
);
}
#[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 detects_trailing_bytes() {
assert_eq!(
require_exact_consumption(1, 2),
Err(DecodeError::TrailingBytes)
);
}
#[test]
fn detects_decoder_overread() {
assert_eq!(
require_exact_consumption(3, 2),
Err(DecodeError::DecoderOverread)
);
}
}