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