btpc_core/
limits.rs

1use crate::{Error, Result};
2
3/// Resource limits applied while parsing untrusted bencode.
4#[allow(clippy::struct_field_names)]
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6pub struct ParseLimits {
7    max_depth: usize,
8    max_items: usize,
9    max_byte_string_length: usize,
10    max_integer_digits: usize,
11    max_total_input: usize,
12    max_owned_allocation: usize,
13}
14
15/// Options applied while loading and owning metainfo.
16#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
17pub struct ParseOptions {
18    limits: ParseLimits,
19}
20
21impl ParseOptions {
22    /// Creates load options from a complete resource-limit configuration.
23    #[must_use]
24    pub const fn new(limits: ParseLimits) -> Self {
25        Self { limits }
26    }
27
28    /// Returns the configured parsing and ownership limits.
29    #[must_use]
30    pub const fn limits(self) -> ParseLimits {
31        self.limits
32    }
33}
34
35#[derive(Debug)]
36pub(crate) struct AllocationBudget {
37    limits: ParseLimits,
38    used: usize,
39}
40
41impl AllocationBudget {
42    pub(crate) const fn new(limits: ParseLimits) -> Self {
43        Self { limits, used: 0 }
44    }
45
46    pub(crate) fn charge(&mut self, amount: usize) -> Result<()> {
47        self.used = self.limits.checked_owned_allocation(self.used, amount)?;
48        Ok(())
49    }
50}
51
52impl ParseLimits {
53    /// Creates a complete parser limit configuration.
54    #[must_use]
55    pub const fn new(
56        max_depth: usize,
57        max_items: usize,
58        max_byte_string_length: usize,
59        max_total_input: usize,
60        max_owned_allocation: usize,
61    ) -> Self {
62        Self {
63            max_depth,
64            max_items,
65            max_byte_string_length,
66            max_integer_digits: 4_096,
67            max_total_input,
68            max_owned_allocation,
69        }
70    }
71
72    /// Returns the maximum container nesting depth.
73    #[must_use]
74    pub const fn max_depth(self) -> usize {
75        self.max_depth
76    }
77
78    /// Returns the maximum number of parsed values.
79    #[must_use]
80    pub const fn max_items(self) -> usize {
81        self.max_items
82    }
83
84    /// Returns the maximum byte-string length.
85    #[must_use]
86    pub const fn max_byte_string_length(self) -> usize {
87        self.max_byte_string_length
88    }
89
90    /// Returns the maximum number of integer digits, excluding a minus sign.
91    #[must_use]
92    pub const fn max_integer_digits(self) -> usize {
93        self.max_integer_digits
94    }
95
96    /// Replaces the maximum integer digit count.
97    #[must_use]
98    pub const fn with_max_integer_digits(mut self, maximum: usize) -> Self {
99        self.max_integer_digits = maximum;
100        self
101    }
102
103    /// Returns the maximum total input length.
104    #[must_use]
105    pub const fn max_total_input(self) -> usize {
106        self.max_total_input
107    }
108
109    /// Returns the maximum budget for allocations made by owned conversion.
110    #[must_use]
111    pub const fn max_owned_allocation(self) -> usize {
112        self.max_owned_allocation
113    }
114
115    /// Checks a nesting depth against the configured maximum.
116    ///
117    /// # Errors
118    ///
119    /// Returns a resource-limit error when `actual` exceeds the maximum depth.
120    pub const fn check_depth(self, actual: usize) -> Result<()> {
121        check_limit("depth", actual, self.max_depth)
122    }
123
124    /// Checks an item count against the configured maximum.
125    ///
126    /// # Errors
127    ///
128    /// Returns a resource-limit error when `actual` exceeds the maximum count.
129    pub const fn check_items(self, actual: usize) -> Result<()> {
130        check_limit("item count", actual, self.max_items)
131    }
132
133    /// Checks a byte-string length against the configured maximum.
134    ///
135    /// # Errors
136    ///
137    /// Returns a resource-limit error when `actual` exceeds the maximum length.
138    pub const fn check_byte_string_length(self, actual: usize) -> Result<()> {
139        check_limit("byte-string length", actual, self.max_byte_string_length)
140    }
141
142    /// Checks an integer digit count against the configured maximum.
143    ///
144    /// # Errors
145    ///
146    /// Returns a resource-limit error when `actual` exceeds the maximum count.
147    pub const fn check_integer_digits(self, actual: usize) -> Result<()> {
148        check_limit("integer digits", actual, self.max_integer_digits)
149    }
150
151    /// Checks total input length against the configured maximum.
152    ///
153    /// # Errors
154    ///
155    /// Returns a resource-limit error when `actual` exceeds the input maximum.
156    pub const fn check_total_input(self, actual: usize) -> Result<()> {
157        check_limit("total input", actual, self.max_total_input)
158    }
159
160    /// Adds an allocation to an accumulated total without overflow.
161    ///
162    /// # Errors
163    ///
164    /// Returns a resource-limit error when addition overflows or the resulting
165    /// total exceeds the owned-allocation budget.
166    pub const fn checked_owned_allocation(self, current: usize, added: usize) -> Result<usize> {
167        let Some(total) = current.checked_add(added) else {
168            return Err(Error::resource_limit(
169                "owned allocation",
170                usize::MAX,
171                self.max_owned_allocation,
172            ));
173        };
174        if total > self.max_owned_allocation {
175            Err(Error::resource_limit(
176                "owned allocation",
177                total,
178                self.max_owned_allocation,
179            ))
180        } else {
181            Ok(total)
182        }
183    }
184}
185
186impl Default for ParseLimits {
187    fn default() -> Self {
188        Self::new(
189            128,
190            1_000_000,
191            128 * 1024 * 1024,
192            256 * 1024 * 1024,
193            256 * 1024 * 1024,
194        )
195    }
196}
197
198const fn check_limit(limit: &'static str, actual: usize, maximum: usize) -> Result<()> {
199    if actual > maximum {
200        Err(Error::resource_limit(limit, actual, maximum))
201    } else {
202        Ok(())
203    }
204}