Skip to main content

eth_valkyoth_codec/
lib.rs

1#![no_std]
2#![forbid(unsafe_code)]
3//! Bounded decoding policy for untrusted Ethereum wire inputs.
4
5use core::cmp::Ordering;
6
7/// Resource limits required by every untrusted decoder.
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub struct DecodeLimits {
10    /// Maximum accepted input length in bytes.
11    pub max_input_bytes: usize,
12    /// Maximum items accepted in any decoded list.
13    pub max_list_items: usize,
14    /// Maximum nested list depth.
15    pub max_nesting_depth: usize,
16    /// Maximum total allocation a decoder may request.
17    pub max_total_allocation: usize,
18}
19
20impl DecodeLimits {
21    /// Conservative defaults for small unit and conformance fixtures.
22    pub const STRICT: Self = Self {
23        max_input_bytes: 1 << 20,
24        max_list_items: 4096,
25        max_nesting_depth: 64,
26        max_total_allocation: 1 << 20,
27    };
28
29    /// Validates the input length before parsing starts.
30    pub fn check_input_len(self, len: usize) -> Result<(), DecodeError> {
31        if len > self.max_input_bytes {
32            return Err(DecodeError::InputTooLarge);
33        }
34        Ok(())
35    }
36
37    /// Validates a decoded list item count.
38    pub fn check_list_count(self, count: usize) -> Result<(), DecodeError> {
39        if count > self.max_list_items {
40            return Err(DecodeError::ListTooLong);
41        }
42        Ok(())
43    }
44
45    /// Validates the current nesting depth.
46    pub fn check_nesting_depth(self, depth: usize) -> Result<(), DecodeError> {
47        if depth > self.max_nesting_depth {
48            return Err(DecodeError::NestingTooDeep);
49        }
50        Ok(())
51    }
52
53    /// Validates a requested allocation against the total allocation budget.
54    pub fn check_allocation(self, size: usize) -> Result<(), DecodeError> {
55        if size > self.max_total_allocation {
56            return Err(DecodeError::AllocationExceeded);
57        }
58        Ok(())
59    }
60}
61
62/// Shared decode failure categories.
63#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64pub enum DecodeError {
65    /// The byte input is larger than the active decode budget.
66    InputTooLarge,
67    /// The input contains trailing bytes after a decoded value.
68    TrailingBytes,
69    /// A decoder reported consuming more bytes than the input contains.
70    DecoderOverread,
71    /// The input is malformed for the selected wire format.
72    Malformed,
73    /// A decoded list contains more items than the active decode budget.
74    ListTooLong,
75    /// Decoding exceeded the active nesting-depth budget.
76    NestingTooDeep,
77    /// A decoder requested allocation beyond the active allocation budget.
78    AllocationExceeded,
79}
80
81/// Ensures a decoder consumed the whole input.
82pub fn require_exact_consumption(consumed: usize, input_len: usize) -> Result<(), DecodeError> {
83    match consumed.cmp(&input_len) {
84        Ordering::Equal => Ok(()),
85        Ordering::Less => Err(DecodeError::TrailingBytes),
86        Ordering::Greater => Err(DecodeError::DecoderOverread),
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn rejects_oversized_input() {
96        let limits = DecodeLimits {
97            max_input_bytes: 2,
98            ..DecodeLimits::STRICT
99        };
100        assert_eq!(limits.check_input_len(3), Err(DecodeError::InputTooLarge));
101    }
102
103    #[test]
104    fn rejects_oversized_list() {
105        let limits = DecodeLimits {
106            max_list_items: 2,
107            ..DecodeLimits::STRICT
108        };
109        assert_eq!(limits.check_list_count(3), Err(DecodeError::ListTooLong));
110    }
111
112    #[test]
113    fn rejects_excessive_nesting_depth() {
114        let limits = DecodeLimits {
115            max_nesting_depth: 2,
116            ..DecodeLimits::STRICT
117        };
118        assert_eq!(
119            limits.check_nesting_depth(3),
120            Err(DecodeError::NestingTooDeep)
121        );
122    }
123
124    #[test]
125    fn rejects_excessive_allocation() {
126        let limits = DecodeLimits {
127            max_total_allocation: 2,
128            ..DecodeLimits::STRICT
129        };
130        assert_eq!(
131            limits.check_allocation(3),
132            Err(DecodeError::AllocationExceeded)
133        );
134    }
135
136    #[test]
137    fn detects_trailing_bytes() {
138        assert_eq!(
139            require_exact_consumption(1, 2),
140            Err(DecodeError::TrailingBytes)
141        );
142    }
143
144    #[test]
145    fn detects_decoder_overread() {
146        assert_eq!(
147            require_exact_consumption(3, 2),
148            Err(DecodeError::DecoderOverread)
149        );
150    }
151}