1#![no_std]
2#![forbid(unsafe_code)]
3#[cfg(feature = "std")]
6extern crate std;
7
8use core::{cmp::Ordering, fmt};
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub struct DecodeLimits {
13 pub max_input_bytes: usize,
15 pub max_list_items: usize,
17 pub max_nesting_depth: usize,
19 pub max_total_allocation: usize,
21}
22
23impl DecodeLimits {
24 pub const TEST_FIXTURE: Self = Self {
29 max_input_bytes: 1 << 20,
30 max_list_items: 4096,
31 max_nesting_depth: 64,
32 max_total_allocation: 1 << 20,
33 };
34
35 pub const PRODUCTION_RECOMMENDED: Self = Self {
40 max_input_bytes: 2 << 20,
41 max_list_items: 16_384,
42 max_nesting_depth: 64,
43 max_total_allocation: 4 << 20,
44 };
45
46 pub fn check_input_len(self, len: usize) -> Result<(), DecodeError> {
48 if len > self.max_input_bytes {
49 return Err(DecodeError::InputTooLarge);
50 }
51 Ok(())
52 }
53
54 pub fn check_list_count(self, count: usize) -> Result<(), DecodeError> {
56 if count > self.max_list_items {
57 return Err(DecodeError::ListTooLong);
58 }
59 Ok(())
60 }
61
62 pub fn check_nesting_depth(self, depth: usize) -> Result<(), DecodeError> {
64 if depth > self.max_nesting_depth {
65 return Err(DecodeError::NestingTooDeep);
66 }
67 Ok(())
68 }
69
70 pub fn check_single_allocation_limit(self, size: usize) -> Result<(), DecodeError> {
76 if size > self.max_total_allocation {
77 return Err(DecodeError::AllocationExceeded);
78 }
79 Ok(())
80 }
81
82 #[must_use]
84 pub const fn accumulator(self) -> DecodeAccumulator {
85 DecodeAccumulator {
86 limits: self,
87 total_allocated: 0,
88 }
89 }
90}
91
92#[derive(Debug, Eq, PartialEq)]
94pub struct DecodeAccumulator {
95 limits: DecodeLimits,
96 total_allocated: usize,
97}
98
99impl DecodeAccumulator {
100 #[must_use]
102 pub const fn limits(&self) -> DecodeLimits {
103 self.limits
104 }
105
106 #[must_use]
108 pub const fn total_allocated(&self) -> usize {
109 self.total_allocated
110 }
111
112 pub fn check_input_len(&self, len: usize) -> Result<(), DecodeError> {
114 self.limits.check_input_len(len)
115 }
116
117 pub fn check_list_count(&self, count: usize) -> Result<(), DecodeError> {
119 self.limits.check_list_count(count)
120 }
121
122 pub fn check_nesting_depth(&self, depth: usize) -> Result<(), DecodeError> {
124 self.limits.check_nesting_depth(depth)
125 }
126
127 pub fn check_allocation(&mut self, size: usize) -> Result<(), DecodeError> {
129 let new_total = self
130 .total_allocated
131 .checked_add(size)
132 .ok_or(DecodeError::AllocationExceeded)?;
133 if new_total > self.limits.max_total_allocation {
134 return Err(DecodeError::AllocationExceeded);
135 }
136 self.total_allocated = new_total;
137 Ok(())
138 }
139}
140
141#[derive(Clone, Copy, Debug, Eq, PartialEq)]
143pub enum DecodeError {
144 InputTooLarge,
146 TrailingBytes,
148 DecoderOverread,
150 Malformed,
152 ListTooLong,
154 NestingTooDeep,
156 AllocationExceeded,
158}
159
160impl DecodeError {
161 #[must_use]
163 pub const fn code(self) -> &'static str {
164 match self {
165 Self::InputTooLarge => "ETH_CODEC_INPUT_TOO_LARGE",
166 Self::TrailingBytes => "ETH_CODEC_TRAILING_BYTES",
167 Self::DecoderOverread => "ETH_CODEC_DECODER_OVERREAD",
168 Self::Malformed => "ETH_CODEC_MALFORMED",
169 Self::ListTooLong => "ETH_CODEC_LIST_TOO_LONG",
170 Self::NestingTooDeep => "ETH_CODEC_NESTING_TOO_DEEP",
171 Self::AllocationExceeded => "ETH_CODEC_ALLOCATION_EXCEEDED",
172 }
173 }
174
175 #[must_use]
177 pub const fn message(self) -> &'static str {
178 match self {
179 Self::InputTooLarge => "input exceeds the active decode byte limit",
180 Self::TrailingBytes => "decoded value did not consume the full input",
181 Self::DecoderOverread => "decoder consumed more bytes than were available",
182 Self::Malformed => "input is malformed for the selected codec",
183 Self::ListTooLong => "decoded list exceeds the active item limit",
184 Self::NestingTooDeep => "decoded structure exceeds the active nesting limit",
185 Self::AllocationExceeded => "decoder exceeded the active allocation limit",
186 }
187 }
188
189 #[must_use]
191 pub const fn category(self) -> DecodeErrorCategory {
192 match self {
193 Self::InputTooLarge
194 | Self::ListTooLong
195 | Self::NestingTooDeep
196 | Self::AllocationExceeded => DecodeErrorCategory::ResourceExhaustion,
197 Self::TrailingBytes | Self::DecoderOverread | Self::Malformed => {
198 DecodeErrorCategory::MalformedInput
199 }
200 }
201 }
202
203 #[must_use]
206 pub const fn resource(self) -> Option<ResourceError> {
207 match self {
208 Self::InputTooLarge => Some(ResourceError::InputBytes),
209 Self::ListTooLong => Some(ResourceError::ListItems),
210 Self::NestingTooDeep => Some(ResourceError::NestingDepth),
211 Self::AllocationExceeded => Some(ResourceError::AllocationBytes),
212 Self::TrailingBytes | Self::DecoderOverread | Self::Malformed => None,
213 }
214 }
215}
216
217impl fmt::Display for DecodeError {
218 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
219 formatter.write_str(self.message())
220 }
221}
222
223#[cfg(feature = "std")]
224impl std::error::Error for DecodeError {}
225
226#[derive(Clone, Copy, Debug, Eq, PartialEq)]
228pub enum DecodeErrorCategory {
229 MalformedInput,
231 ResourceExhaustion,
233}
234
235#[derive(Clone, Copy, Debug, Eq, PartialEq)]
237pub enum ResourceError {
238 InputBytes,
240 ListItems,
242 NestingDepth,
244 AllocationBytes,
246}
247
248impl ResourceError {
249 #[must_use]
251 pub const fn code(self) -> &'static str {
252 match self {
253 Self::InputBytes => "ETH_RESOURCE_INPUT_BYTES",
254 Self::ListItems => "ETH_RESOURCE_LIST_ITEMS",
255 Self::NestingDepth => "ETH_RESOURCE_NESTING_DEPTH",
256 Self::AllocationBytes => "ETH_RESOURCE_ALLOCATION_BYTES",
257 }
258 }
259
260 #[must_use]
262 pub const fn message(self) -> &'static str {
263 match self {
264 Self::InputBytes => "input byte budget exceeded",
265 Self::ListItems => "list item budget exceeded",
266 Self::NestingDepth => "nesting depth budget exceeded",
267 Self::AllocationBytes => "allocation byte budget exceeded",
268 }
269 }
270}
271
272impl fmt::Display for ResourceError {
273 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
274 formatter.write_str(self.message())
275 }
276}
277
278#[cfg(feature = "std")]
279impl std::error::Error for ResourceError {}
280
281pub fn require_exact_consumption(consumed: usize, input_len: usize) -> Result<(), DecodeError> {
283 match consumed.cmp(&input_len) {
284 Ordering::Equal => Ok(()),
285 Ordering::Less => Err(DecodeError::TrailingBytes),
286 Ordering::Greater => Err(DecodeError::DecoderOverread),
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 extern crate std;
294 use std::string::ToString;
295
296 #[test]
297 fn rejects_oversized_input() {
298 let limits = DecodeLimits {
299 max_input_bytes: 2,
300 ..DecodeLimits::TEST_FIXTURE
301 };
302 assert_eq!(limits.check_input_len(3), Err(DecodeError::InputTooLarge));
303 }
304
305 #[test]
306 fn rejects_oversized_list() {
307 let limits = DecodeLimits {
308 max_list_items: 2,
309 ..DecodeLimits::TEST_FIXTURE
310 };
311 assert_eq!(limits.check_list_count(3), Err(DecodeError::ListTooLong));
312 }
313
314 #[test]
315 fn rejects_excessive_nesting_depth() {
316 let limits = DecodeLimits {
317 max_nesting_depth: 2,
318 ..DecodeLimits::TEST_FIXTURE
319 };
320 assert_eq!(
321 limits.check_nesting_depth(3),
322 Err(DecodeError::NestingTooDeep)
323 );
324 }
325
326 #[test]
327 fn rejects_excessive_allocation() {
328 let limits = DecodeLimits {
329 max_total_allocation: 2,
330 ..DecodeLimits::TEST_FIXTURE
331 };
332 assert_eq!(
333 limits.check_single_allocation_limit(3),
334 Err(DecodeError::AllocationExceeded)
335 );
336 }
337
338 #[test]
339 fn fixture_and_production_limits_are_distinct() {
340 let production = DecodeLimits::PRODUCTION_RECOMMENDED;
341 let fixture = DecodeLimits::TEST_FIXTURE;
342
343 assert!(production.max_input_bytes > fixture.max_input_bytes);
344 assert!(production.max_total_allocation > fixture.max_total_allocation);
345 }
346
347 #[test]
348 fn decode_errors_have_stable_codes_and_messages() {
349 assert_eq!(DecodeError::Malformed.code(), "ETH_CODEC_MALFORMED");
350 assert_eq!(
351 DecodeError::Malformed.message(),
352 "input is malformed for the selected codec"
353 );
354 assert_eq!(
355 DecodeError::Malformed.category(),
356 DecodeErrorCategory::MalformedInput
357 );
358 assert_eq!(
359 DecodeError::Malformed.to_string(),
360 "input is malformed for the selected codec"
361 );
362 }
363
364 #[test]
365 fn resource_errors_are_classified_without_payloads() {
366 let error = DecodeError::AllocationExceeded;
367
368 assert_eq!(error.category(), DecodeErrorCategory::ResourceExhaustion);
369 assert_eq!(error.resource(), Some(ResourceError::AllocationBytes));
370 assert_eq!(
371 ResourceError::AllocationBytes.code(),
372 "ETH_RESOURCE_ALLOCATION_BYTES"
373 );
374 assert_eq!(
375 ResourceError::AllocationBytes.to_string(),
376 "allocation byte budget exceeded"
377 );
378 }
379
380 #[test]
381 fn accumulator_rejects_cumulative_allocation_over_budget() {
382 let limits = DecodeLimits {
383 max_total_allocation: 4,
384 ..DecodeLimits::TEST_FIXTURE
385 };
386 let mut accumulator = limits.accumulator();
387
388 assert_eq!(accumulator.check_allocation(3), Ok(()));
389 assert_eq!(accumulator.total_allocated(), 3);
390 assert_eq!(
391 accumulator.check_allocation(2),
392 Err(DecodeError::AllocationExceeded)
393 );
394 assert_eq!(accumulator.total_allocated(), 3);
395 }
396
397 #[test]
398 fn accumulator_rejects_allocation_counter_overflow() {
399 let limits = DecodeLimits {
400 max_total_allocation: usize::MAX,
401 ..DecodeLimits::TEST_FIXTURE
402 };
403 let mut accumulator = limits.accumulator();
404
405 assert_eq!(accumulator.check_allocation(usize::MAX), Ok(()));
406 assert_eq!(
407 accumulator.check_allocation(1),
408 Err(DecodeError::AllocationExceeded)
409 );
410 }
411
412 #[test]
413 fn detects_trailing_bytes() {
414 assert_eq!(
415 require_exact_consumption(1, 2),
416 Err(DecodeError::TrailingBytes)
417 );
418 }
419
420 #[test]
421 fn detects_decoder_overread() {
422 assert_eq!(
423 require_exact_consumption(3, 2),
424 Err(DecodeError::DecoderOverread)
425 );
426 }
427}