#![no_std]
#![forbid(unsafe_code)]
//! Bounded decoding policy for untrusted Ethereum wire inputs.

#[cfg(feature = "std")]
extern crate std;

use core::{cmp::Ordering, fmt};

/// Resource limits required by every untrusted decoder.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DecodeLimits {
    /// Maximum accepted input length in bytes.
    pub max_input_bytes: usize,
    /// Maximum items accepted in any decoded list.
    pub max_list_items: usize,
    /// Maximum nested list depth.
    pub max_nesting_depth: usize,
    /// Maximum total allocation a decoder may request.
    pub max_total_allocation: usize,
}

impl DecodeLimits {
    /// Limits for unit tests and conformance fixtures.
    ///
    /// This is not a production policy. Production decoders should choose
    /// deployment-specific limits or start from [`Self::PRODUCTION_RECOMMENDED`].
    pub const TEST_FIXTURE: Self = Self {
        max_input_bytes: 1 << 20,
        max_list_items: 4096,
        max_nesting_depth: 64,
        max_total_allocation: 1 << 20,
    };

    /// Recommended starting point for production wire decoders.
    ///
    /// Review and tighten these values per deployment context before relying
    /// on them for externally reachable services.
    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,
    };

    /// Validates the input length before parsing starts.
    pub fn check_input_len(self, len: usize) -> Result<(), DecodeError> {
        if len > self.max_input_bytes {
            return Err(DecodeError::InputTooLarge);
        }
        Ok(())
    }

    /// Validates a decoded list item count.
    pub fn check_list_count(self, count: usize) -> Result<(), DecodeError> {
        if count > self.max_list_items {
            return Err(DecodeError::ListTooLong);
        }
        Ok(())
    }

    /// Validates the current nesting depth.
    pub fn check_nesting_depth(self, depth: usize) -> Result<(), DecodeError> {
        if depth > self.max_nesting_depth {
            return Err(DecodeError::NestingTooDeep);
        }
        Ok(())
    }

    /// Validates one requested allocation against the allocation budget.
    ///
    /// This helper is for single-allocation checks only. Decoders that can make
    /// more than one allocation must use [`DecodeAccumulator`] to enforce the
    /// cumulative budget.
    pub fn check_single_allocation_limit(self, size: usize) -> Result<(), DecodeError> {
        if size > self.max_total_allocation {
            return Err(DecodeError::AllocationExceeded);
        }
        Ok(())
    }

    /// Starts stateful budget accounting for a decoder invocation.
    #[must_use]
    pub const fn accumulator(self) -> DecodeAccumulator {
        DecodeAccumulator {
            limits: self,
            total_allocated: 0,
        }
    }
}

/// Stateful budget accounting for one decoder invocation.
#[derive(Debug, Eq, PartialEq)]
pub struct DecodeAccumulator {
    limits: DecodeLimits,
    total_allocated: usize,
}

impl DecodeAccumulator {
    /// Returns the active decode limits.
    #[must_use]
    pub const fn limits(&self) -> DecodeLimits {
        self.limits
    }

    /// Returns the cumulative allocation accounted so far.
    #[must_use]
    pub const fn total_allocated(&self) -> usize {
        self.total_allocated
    }

    /// Validates the input length before parsing starts.
    pub fn check_input_len(&self, len: usize) -> Result<(), DecodeError> {
        self.limits.check_input_len(len)
    }

    /// Validates a decoded list item count.
    pub fn check_list_count(&self, count: usize) -> Result<(), DecodeError> {
        self.limits.check_list_count(count)
    }

    /// Validates the current nesting depth.
    pub fn check_nesting_depth(&self, depth: usize) -> Result<(), DecodeError> {
        self.limits.check_nesting_depth(depth)
    }

    /// Accounts for one allocation against the cumulative allocation budget.
    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(())
    }
}

/// Shared decode failure categories.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecodeError {
    /// The byte input is larger than the active decode budget.
    InputTooLarge,
    /// The input contains trailing bytes after a decoded value.
    TrailingBytes,
    /// A decoder reported consuming more bytes than the input contains.
    DecoderOverread,
    /// The input is malformed for the selected wire format.
    Malformed,
    /// A decoded list contains more items than the active decode budget.
    ListTooLong,
    /// Decoding exceeded the active nesting-depth budget.
    NestingTooDeep,
    /// A decoder requested allocation beyond the active allocation budget.
    AllocationExceeded,
}

impl DecodeError {
    /// Stable machine-readable error code.
    #[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",
        }
    }

    /// Stable human-readable error message.
    #[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",
        }
    }

    /// Stable high-level category for policy decisions.
    #[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
            }
        }
    }

    /// Returns the resource budget that was exceeded, if this is a resource
    /// exhaustion error.
    #[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 {}

/// Stable high-level decode error categories.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecodeErrorCategory {
    /// The input is malformed or internally inconsistent.
    MalformedInput,
    /// The input exceeded an explicit resource budget.
    ResourceExhaustion,
}

/// Stable resource budget categories.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ResourceError {
    /// Input byte limit was exceeded.
    InputBytes,
    /// Decoded list item limit was exceeded.
    ListItems,
    /// Nesting depth limit was exceeded.
    NestingDepth,
    /// Cumulative allocation limit was exceeded.
    AllocationBytes,
}

impl ResourceError {
    /// Stable machine-readable error code.
    #[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",
        }
    }

    /// Stable human-readable error message.
    #[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 {}

/// Ensures a decoder consumed the whole input.
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)
        );
    }
}
