Skip to main content

mlt_core/decoder/
limits.rs

1//! Memory-limit enforcement shared by every layer format.
2
3use crate::errors::AsMltError as _;
4use crate::{Layer, MltError, MltResult, ParsedLayer};
5
6/// Default memory budget: 20 MiB.
7const DEFAULT_MAX_BYTES: u32 = 20 * 1024 * 1024;
8
9/// Stateful decoder that enforces a per-tile memory budget during decoding.
10///
11/// Pass a `Decoder` to every `raw.decode()` / `into_tile()` call and to `from_bytes`-style parsers.
12/// Each method charges the budget before performing heap allocations, so the total heap used never exceeds `max_bytes` (in bytes).
13#[derive(Debug, Clone, PartialEq, Eq, Default)]
14pub struct Decoder {
15    /// Keep track of the memory used when decoding a tile: raw->parsed transition
16    budget: MemBudget,
17    /// Reusable scratch buffer for the physical u32 decode pass.
18    /// Held here so its heap allocation is reused across streams without extra cost.
19    pub(crate) buffer_u32: Vec<u32>,
20    /// Reusable scratch buffer for the physical u64 decode pass.
21    /// Held here so its heap allocation is reused across streams without extra cost.
22    pub(crate) buffer_u64: Vec<u64>,
23}
24
25impl Decoder {
26    /// Create a decoder with a custom memory budget (in bytes).
27    #[must_use]
28    pub fn with_max_size(max_bytes: u32) -> Self {
29        Self {
30            budget: MemBudget::with_max_size(max_bytes),
31            ..Default::default()
32        }
33    }
34
35    pub fn decode_all<'a>(
36        &mut self,
37        layers: impl IntoIterator<Item = Layer<'a>>,
38    ) -> MltResult<Vec<ParsedLayer<'a>>> {
39        layers
40            .into_iter()
41            .map(|l| l.decode_all(self))
42            .collect::<MltResult<_>>()
43    }
44
45    /// Allocate a `Vec<T>` with the given capacity, charging the decoder's budget for `capacity * size_of::<T>()` bytes.
46    /// Use this instead of `Vec::with_capacity` in decode paths.
47    #[inline]
48    pub(crate) fn alloc<T>(&mut self, capacity: usize) -> MltResult<Vec<T>> {
49        let bytes = capacity.checked_mul(size_of::<T>()).or_overflow()?;
50        let bytes_u32 = u32::try_from(bytes).or_overflow()?;
51        self.budget.consume(bytes_u32)?;
52        Ok(Vec::with_capacity(capacity))
53    }
54
55    /// Charge the budget for `size` raw bytes.
56    /// Prefer [`consume_items`][Self::consume_items] when charging for a known-type collection.
57    #[inline]
58    pub(crate) fn consume(&mut self, size: u32) -> MltResult<()> {
59        self.budget.consume(size)
60    }
61
62    /// Charge the budget for `count` items of type `T` (`count * size_of::<T>()` bytes).
63    #[inline]
64    pub(crate) fn consume_items<T>(&mut self, count: usize) -> MltResult<()> {
65        let bytes = count.checked_mul(size_of::<T>()).or_overflow()?;
66        self.budget.consume(u32::try_from(bytes).or_overflow()?)
67    }
68
69    #[inline]
70    pub(crate) fn adjust(&mut self, adjustment: u32) {
71        self.budget.adjust(adjustment);
72    }
73
74    /// Return the unused portion of a pre-charged allocation budget.
75    ///
76    /// Call this after fully populating a `Vec<T>` that was pre-allocated with [`Decoder::alloc`],
77    /// passing the same `alloc_size` that was given to `alloc`.
78    ///
79    /// Returns an error if the vector grew beyond `alloc_size` (malformed input caused more items
80    /// than declared). Subtracts `(alloc_size - buf.len()) * size_of::<T>()` from the budget.
81    #[inline]
82    pub(crate) fn adjust_alloc<T>(&mut self, buf: &[T], alloc_size: usize) -> MltResult<()> {
83        if buf.len() > alloc_size {
84            return Err(MltError::InvalidDecodingStreamSize(buf.len(), alloc_size));
85        }
86        // Return the unused portion of the pre-charged budget.
87        let unused = (alloc_size - buf.len()) * size_of::<T>();
88        // unused fits in u32: it's at most alloc_size * size_of::<T>(), which was checked to fit
89        // in u32 when alloc() was called. Using saturating_cast to avoid a fallible conversion.
90        #[expect(
91            clippy::cast_possible_truncation,
92            reason = "unused <= alloc_size * size_of::<T>() which was verified to fit in u32 by alloc()"
93        )]
94        self.budget.adjust(unused as u32);
95        Ok(())
96    }
97
98    #[must_use]
99    pub fn consumed(&self) -> u32 {
100        self.budget.consumed()
101    }
102
103    /// Reset the memory budget to zero, keeping scratch buffers allocated.
104    ///
105    /// Call this between tiles when reusing a single `Decoder` for multiple
106    /// decodes - the per-tile budget is enforced fresh, but the internal
107    /// `buffer_u32` / `buffer_u64` scratch space is retained so it doesn't
108    /// need to be re-allocated.
109    ///
110    /// # Safety / correctness precondition
111    ///
112    /// Only call this after dropping any decoded allocations returned from the
113    /// previous tile. Resetting the budget while earlier decoded outputs are
114    /// still alive makes the budget enforceable only per-tile and can bypass
115    /// the stronger guarantee that total live heap tracked by this decoder
116    /// never exceeds the configured maximum.
117    pub fn reset_budget(&mut self) {
118        self.budget.reset();
119    }
120}
121
122/// Stateful parser that enforces a memory budget during parsing (binary -> raw structures).
123///
124/// The parse chain reserves memory before allocations so total heap stays within the limit.
125///
126/// ```
127/// use mlt_core::Parser;
128///
129/// # let bytes: &[u8] = &[];
130/// let mut parser = Parser::default();
131/// let layers = parser.parse_layers(bytes).expect("parse");
132///
133/// // Or with a custom limit:
134/// let mut parser = Parser::with_max_size(64 * 1024 * 1024);
135/// ```
136#[derive(Debug, Clone, PartialEq, Eq, Default)]
137pub struct Parser {
138    budget: MemBudget,
139}
140
141impl Parser {
142    /// Create a parser with a custom memory budget (in bytes).
143    #[must_use]
144    pub fn with_max_size(max_bytes: u32) -> Self {
145        Self {
146            budget: MemBudget::with_max_size(max_bytes),
147        }
148    }
149
150    /// Parse a sequence of binary layers, reserving decoded memory against this parser's budget.
151    pub fn parse_layers<'a>(&mut self, mut input: &'a [u8]) -> MltResult<Vec<Layer<'a>>> {
152        let mut result = Vec::new();
153        while !input.is_empty() {
154            let layer;
155            (input, layer) = Layer::from_bytes(input, self)?;
156            result.push(layer);
157        }
158        Ok(result)
159    }
160
161    /// Reserve `size` bytes from the parse budget. Used internally by the parse chain.
162    #[inline]
163    pub(crate) fn reserve(&mut self, size: u32) -> MltResult<()> {
164        self.budget.consume(size)
165    }
166
167    #[must_use]
168    pub fn reserved(&self) -> u32 {
169        self.budget.consumed()
170    }
171}
172
173/// A presence field that is not a bitmap is built rather than borrowed, so what it builds
174/// is charged here before it is allocated - a few bytes of runs can name any feature count.
175#[cfg(feature = "unstable-v2")]
176impl crate::codecs::presence_coding::PresenceBudget for Parser {
177    fn reserve_bits(&mut self, count: u32) -> MltResult<()> {
178        self.reserve(count.div_ceil(8))
179    }
180}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
183struct MemBudget {
184    /// Hard ceiling: total decoded bytes may not exceed this value.
185    pub max_bytes: u32,
186    /// Running total of used bytes so far.
187    pub bytes_used: u32,
188}
189
190impl Default for MemBudget {
191    /// Create a decoder with the default 10 MiB memory budget.
192    fn default() -> Self {
193        Self::with_max_size(DEFAULT_MAX_BYTES)
194    }
195}
196
197impl MemBudget {
198    /// Create a decoder with a custom memory budget (in bytes).
199    #[must_use]
200    fn with_max_size(max_bytes: u32) -> Self {
201        Self {
202            max_bytes,
203            bytes_used: 0,
204        }
205    }
206
207    /// Adjust previous consumption by `- adjustment` bytes.  Will panic if used incorrectly.
208    #[inline]
209    fn adjust(&mut self, adjustment: u32) {
210        self.bytes_used = self.bytes_used.checked_sub(adjustment).unwrap();
211    }
212
213    /// Take `size` bytes from the allocation budget. Call this before the actual allocation.
214    #[inline]
215    fn consume(&mut self, size: u32) -> MltResult<()> {
216        let accumulator = &mut self.bytes_used;
217        let max_bytes = self.max_bytes;
218        if let Some(new_value) = accumulator.checked_add(size).filter(|&v| v <= max_bytes) {
219            *accumulator = new_value;
220            Ok(())
221        } else {
222            Err(MltError::MemoryLimitExceeded {
223                limit: max_bytes,
224                used: *accumulator,
225                requested: size,
226            })
227        }
228    }
229
230    fn consumed(&self) -> u32 {
231        self.bytes_used
232    }
233
234    /// Reset tracked usage for a new decode window.
235    ///
236    /// Callers must ensure that allocations accounted for by the previous
237    /// window are no longer live before resetting.
238    fn reset(&mut self) {
239        self.bytes_used = 0;
240    }
241}