Skip to main content

cadmpeg_core/decode/
context.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Decode state, decompression limits, and session lifecycle.
3
4use std::cell::Cell;
5
6use crate::{CodecError, ReadSeek};
7
8use super::arena::DecodeArena;
9use super::budget::{DecodeBudget, DepthGuard, ScopedReservation, WorkBudget};
10use super::error::{
11    ErrorContext, LimitScope, ResourceDimension, ResourceFailure, ResourceLimit, SourceLocation,
12};
13use super::policy::{
14    DecodePolicy, DECOMPRESSED_PER_EXPAND_BASE, DECOMPRESSED_PER_EXPAND_PER_INPUT_BYTE,
15};
16use super::space::{ByteRange, SpaceId};
17use super::view::View;
18
19/// Initial per-expand reservation clamp: an attacker's declared size cannot
20/// force a large up-front reservation before any output is produced.
21const RESERVE_CLAMP: u64 = 8 * 1024 * 1024;
22
23/// Shared monotonic decode state.
24#[derive(Debug)]
25pub struct DecodeContext<'a> {
26    arena: &'a DecodeArena,
27    policy: DecodePolicy,
28    container_only: bool,
29    budget: DecodeBudget,
30    next_space: Cell<u32>,
31}
32
33impl<'a> DecodeContext<'a> {
34    /// Reads the root input under `max_input_bytes`, copies it into the arena,
35    /// registers the root space, establishes input-proportional allowances,
36    /// and returns the context and root view.
37    pub fn read_root(
38        reader: &mut dyn ReadSeek,
39        arena: &'a DecodeArena,
40        policy: &DecodePolicy,
41    ) -> Result<(Self, View<'a>), CodecError> {
42        let max = policy.limits.max_input_bytes;
43        let cap = max.saturating_add(1);
44        let mut buffer: Vec<u8> = Vec::new();
45        let mut chunk = [0u8; 8192];
46        loop {
47            let remaining = cap.saturating_sub(buffer.len() as u64);
48            if remaining == 0 {
49                break;
50            }
51            let want = usize::try_from(remaining.min(chunk.len() as u64)).unwrap_or(chunk.len());
52            let read = reader.read(&mut chunk[..want]).map_err(CodecError::Io)?;
53            if read == 0 {
54                break;
55            }
56            buffer
57                .try_reserve(read)
58                .map_err(|_| root_error(ResourceFailure::AllocationFailed, max, read as u64))?;
59            buffer.extend_from_slice(&chunk[..read]);
60        }
61        if buffer.len() as u64 > max {
62            return Err(root_error(
63                ResourceFailure::BudgetExceeded,
64                max,
65                buffer.len() as u64,
66            ));
67        }
68        let bytes = arena.alloc(buffer.into_boxed_slice());
69        Self::from_bytes(bytes, arena, policy)
70    }
71
72    /// Builds a context over caller-owned root bytes, for fuzz targets and
73    /// tests. The arena still backs any expansions produced during decode.
74    pub fn from_root_bytes(
75        bytes: &'a [u8],
76        arena: &'a DecodeArena,
77        policy: &DecodePolicy,
78    ) -> Result<(Self, View<'a>), CodecError> {
79        Self::from_bytes(bytes, arena, policy)
80    }
81
82    fn from_bytes(
83        bytes: &'a [u8],
84        arena: &'a DecodeArena,
85        policy: &DecodePolicy,
86    ) -> Result<(Self, View<'a>), CodecError> {
87        let length = bytes.len() as u64;
88        if length > policy.limits.max_input_bytes {
89            return Err(root_error(
90                ResourceFailure::BudgetExceeded,
91                policy.limits.max_input_bytes,
92                length,
93            ));
94        }
95        let ctx = DecodeContext {
96            arena,
97            policy: *policy,
98            container_only: false,
99            budget: DecodeBudget::new(*policy, length),
100            next_space: Cell::new(1),
101        };
102        Ok((ctx, View::over_space(bytes, SpaceId::ROOT)))
103    }
104
105    /// Returns the decode policy in force.
106    pub fn policy(&self) -> &DecodePolicy {
107        &self.policy
108    }
109
110    /// Returns whether the caller requested container-only decoding.
111    pub fn container_only(&self) -> bool {
112        self.container_only
113    }
114
115    /// Records the caller's container-only request before decoding begins.
116    pub fn set_container_only(&mut self, value: bool) {
117        self.container_only = value;
118    }
119
120    fn decompression_allowance(&self) -> u64 {
121        self.budget.decompression_allowance()
122    }
123
124    fn per_expand_allowance(&self) -> u64 {
125        let proportional = DECOMPRESSED_PER_EXPAND_BASE.saturating_add(
126            DECOMPRESSED_PER_EXPAND_PER_INPUT_BYTE.saturating_mul(self.budget.input_bytes()),
127        );
128        self.policy
129            .limits
130            .max_decompressed_bytes_per_expand
131            .min(proportional)
132    }
133
134    fn allocate_space(&self) -> SpaceId {
135        let index = self.next_space.get();
136        self.next_space.set(index.saturating_add(1));
137        SpaceId::from_index(index)
138    }
139
140    fn charge_decompressed(
141        &self,
142        scope: LimitScope,
143        amount: u64,
144        operation: &'static str,
145        location: Option<SourceLocation>,
146    ) -> Result<(), CodecError> {
147        debug_assert_eq!(scope, LimitScope::Global);
148        self.budget.charge_decompressed(amount, operation, location)
149    }
150
151    /// Records a permanent fuse and returns the resource error to propagate.
152    fn fuse(
153        &self,
154        reason: ResourceFailure,
155        scope: LimitScope,
156        amount: u64,
157        operation: &'static str,
158        location: Option<SourceLocation>,
159    ) -> CodecError {
160        let limit = match scope {
161            LimitScope::Global => self.decompression_allowance(),
162            LimitScope::PerExpand => self.per_expand_allowance(),
163        };
164        self.budget.refuse(
165            ResourceDimension::DecompressedBytes,
166            reason,
167            scope,
168            limit,
169            self.budget.decompressed_used(),
170            amount,
171            operation,
172            location,
173        )
174    }
175
176    /// Reserves bytes held by a temporary materialization.
177    pub fn reserve_scoped(
178        &self,
179        bytes: u64,
180        operation: &'static str,
181        location: Option<SourceLocation>,
182    ) -> Result<ScopedReservation<'_>, CodecError> {
183        self.budget.reserve_scoped(bytes, operation, location)
184    }
185
186    /// Charges bytes retained for the remainder of this session.
187    pub fn charge_retained(
188        &self,
189        bytes: u64,
190        operation: &'static str,
191        location: Option<SourceLocation>,
192    ) -> Result<(), CodecError> {
193        self.budget.charge_retained(bytes, operation, location)
194    }
195
196    /// Copies bytes into session-retained storage after charging and reserving safely.
197    pub fn copy_retained(
198        &self,
199        bytes: &[u8],
200        operation: &'static str,
201        location: Option<SourceLocation>,
202    ) -> Result<Vec<u8>, CodecError> {
203        self.charge_retained(bytes.len() as u64, operation, location)?;
204        let mut copy = Vec::new();
205        copy.try_reserve_exact(bytes.len()).map_err(|_| {
206            self.fuse(
207                ResourceFailure::AllocationFailed,
208                LimitScope::Global,
209                bytes.len() as u64,
210                operation,
211                location,
212            )
213        })?;
214        copy.extend_from_slice(bytes);
215        Ok(copy)
216    }
217
218    /// Charges admitted entities.
219    pub fn charge_entities(&self, count: u64, operation: &'static str) -> Result<(), CodecError> {
220        self.budget.charge_entities(count, operation)
221    }
222
223    /// Charges admitted collection items.
224    pub fn charge_collection_items(
225        &self,
226        count: u64,
227        operation: &'static str,
228    ) -> Result<(), CodecError> {
229        self.budget.charge_collection_items(count, operation)
230    }
231
232    /// Enters one recursive nesting level until the returned guard is dropped.
233    pub fn enter_nested(
234        &self,
235        operation: &'static str,
236        location: Option<SourceLocation>,
237    ) -> Result<DepthGuard<'_>, CodecError> {
238        self.budget.enter_nested(operation, location)
239    }
240
241    /// Charges session-global algorithm work, fusing on refusal.
242    pub fn charge_work(&self, units: u64, operation: &'static str) -> Result<(), CodecError> {
243        self.budget.charge_work(units, operation)
244    }
245
246    /// Creates a local work slice that also draws from the session allowance.
247    pub fn work_budget(&self, local_limit: u64) -> WorkBudget<'_> {
248        WorkBudget::for_session(local_limit, &self.budget)
249    }
250
251    // --- decompression ------------------------------------------------------
252
253    /// Begins an expansion whose output is charged incrementally and becomes
254    /// available only after successful finalization.
255    pub fn begin_expand(
256        &self,
257        source: View<'_>,
258        spec: ExpandSpec,
259    ) -> Result<ExpandWriter<'_, 'a>, CodecError> {
260        if let Some(limit) = self.budget.fused() {
261            return Err(CodecError::ResourceLimit(limit));
262        }
263        if let ExpandSpec::Exact(size) = spec {
264            let per_expand = self.per_expand_allowance();
265            if size > per_expand {
266                return Err(self.fuse(
267                    ResourceFailure::BudgetExceeded,
268                    LimitScope::PerExpand,
269                    size,
270                    "begin_expand",
271                    Some(source.location()),
272                ));
273            }
274            if size
275                > self
276                    .decompression_allowance()
277                    .saturating_sub(self.budget.decompressed_used())
278            {
279                return Err(self.fuse(
280                    ResourceFailure::BudgetExceeded,
281                    LimitScope::Global,
282                    size,
283                    "begin_expand",
284                    Some(source.location()),
285                ));
286            }
287        }
288        let mut buffer: Vec<u8> = Vec::new();
289        let reserve = match spec {
290            ExpandSpec::Exact(size) => size.min(RESERVE_CLAMP),
291            ExpandSpec::Unknown => 0,
292        };
293        if reserve > 0 {
294            let reserve = usize::try_from(reserve).unwrap_or(usize::MAX);
295            buffer.try_reserve(reserve).map_err(|_| {
296                self.fuse(
297                    ResourceFailure::AllocationFailed,
298                    LimitScope::PerExpand,
299                    reserve as u64,
300                    "begin_expand",
301                    Some(source.location()),
302                )
303            })?;
304        }
305        Ok(ExpandWriter {
306            ctx: self,
307            spec,
308            location: source.location(),
309            buffer,
310            written: 0,
311        })
312    }
313
314    /// Copies several input extents into one derived view.
315    pub fn concat_views(&self, inputs: &[View<'_>]) -> Result<View<'a>, CodecError> {
316        if let Some(limit) = self.budget.fused() {
317            return Err(CodecError::ResourceLimit(limit));
318        }
319        let location = inputs.first().copied().map(View::location);
320        let total = inputs.iter().try_fold(0usize, |total, view| {
321            total.checked_add(view.window().len()).ok_or_else(|| {
322                self.budget.refuse(
323                    ResourceDimension::RetainedBytes,
324                    ResourceFailure::BudgetExceeded,
325                    LimitScope::Global,
326                    self.policy.limits.max_retained_bytes,
327                    total as u64,
328                    view.window().len() as u64,
329                    "concat_views",
330                    location,
331                )
332            })
333        })?;
334        let reservation = self.reserve_scoped(total as u64, "concat_views", location)?;
335        let mut buffer = Vec::new();
336        buffer.try_reserve_exact(total).map_err(|_| {
337            self.budget.refuse(
338                ResourceDimension::MaterializedBytes,
339                ResourceFailure::AllocationFailed,
340                LimitScope::Global,
341                self.policy.limits.max_materialized_bytes,
342                0,
343                total as u64,
344                "concat_views",
345                location,
346            )
347        })?;
348        for view in inputs {
349            buffer.extend_from_slice(view.window());
350        }
351        let bytes = self.arena.alloc(buffer.into_boxed_slice());
352        reservation.commit()?;
353        let space = self.allocate_space();
354        Ok(View::over_space(bytes, space))
355    }
356
357    /// Registers a stored (uncompressed) child range as a space that borrows
358    /// the parent bytes without copying.
359    ///
360    /// `range` is expressed in the parent view's own space coordinates and must
361    /// lie within the parent window; a range that escapes the parent is refused
362    /// here, at the request site, exactly as [`View::child`] refuses. No bytes
363    /// are copied. It is the archive-entry counterpart of [`DecodeContext::begin_expand`] —
364    /// stored ZIP entries take this path, compressed ones take the expander.
365    /// Registration still refuses on a fused context so a stored entry cannot be
366    /// admitted after a refusal.
367    pub fn register_slice<'v>(
368        &self,
369        parent: View<'v>,
370        range: ByteRange,
371    ) -> Result<View<'v>, CodecError> {
372        if let Some(limit) = self.budget.fused() {
373            return Err(CodecError::ResourceLimit(limit));
374        }
375        let start = usize::try_from(range.start).ok();
376        let end = usize::try_from(range.end).ok();
377        let child = start
378            .zip(end)
379            .and_then(|(start, end)| parent.child(start, end))
380            .ok_or_else(|| {
381                CodecError::Malformed(format!(
382                    "stored slice [{}, {}) escapes parent space {}",
383                    range.start,
384                    range.end,
385                    parent.space().index()
386                ))
387            })?;
388        let space = self.allocate_space();
389        Ok(View::over_space(child.window(), space))
390    }
391
392    // --- lifecycle ----------------------------------------------------------
393
394    /// Closes a decode or inspection session, returning a fused resource error
395    /// even when codec code swallowed the charge that caused it.
396    pub fn finish_session(self) -> Result<(), CodecError> {
397        if let Some(limit) = self.budget.fused() {
398            return Err(CodecError::ResourceLimit(limit));
399        }
400        Ok(())
401    }
402}
403
404/// Builds the root-input resource error before a context exists.
405fn root_error(reason: ResourceFailure, limit: u64, used: u64) -> CodecError {
406    CodecError::ResourceLimit(ResourceLimit {
407        dimension: ResourceDimension::InputBytes,
408        reason,
409        scope: LimitScope::Global,
410        limit,
411        used,
412        additional: used.saturating_sub(limit),
413        context: ErrorContext {
414            operation: "read_root",
415            location: None,
416        },
417    })
418}
419
420/// How much output an expansion is expected to produce.
421#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422pub enum ExpandSpec {
423    /// A declared exact size, enforced per-write and at finalize.
424    Exact(u64),
425    /// No trustworthy declared size: the decompression limits apply.
426    Unknown,
427}
428
429/// Writes decompressed output under incremental charging.
430#[derive(Debug)]
431pub struct ExpandWriter<'ctx, 'a> {
432    ctx: &'ctx DecodeContext<'a>,
433    spec: ExpandSpec,
434    location: SourceLocation,
435    buffer: Vec<u8>,
436    written: u64,
437}
438
439impl<'a> ExpandWriter<'_, 'a> {
440    /// Appends decompressed output, charging before it is retained.
441    pub fn write(&mut self, data: &[u8]) -> Result<(), CodecError> {
442        let len = data.len() as u64;
443        let new_written = self.written.saturating_add(len);
444        match self.spec {
445            ExpandSpec::Exact(size) if new_written > size => {
446                return Err(CodecError::Malformed(format!(
447                    "expansion exceeded declared exact size {size}"
448                )))
449            }
450            _ => {}
451        }
452        let per_expand = self.ctx.per_expand_allowance();
453        if new_written > per_expand {
454            return Err(self.ctx.fuse(
455                ResourceFailure::BudgetExceeded,
456                LimitScope::PerExpand,
457                len,
458                "expand_write",
459                Some(self.location),
460            ));
461        }
462        self.ctx.charge_decompressed(
463            LimitScope::Global,
464            len,
465            "expand_write",
466            Some(self.location),
467        )?;
468        self.buffer.try_reserve(data.len()).map_err(|_| {
469            self.ctx.fuse(
470                ResourceFailure::AllocationFailed,
471                LimitScope::PerExpand,
472                len,
473                "expand_write",
474                Some(self.location),
475            )
476        })?;
477        self.buffer.extend_from_slice(data);
478        self.written = new_written;
479        Ok(())
480    }
481
482    /// Finalizes the expansion, stores it in the arena, and registers its space.
483    pub fn finalize(self) -> Result<View<'a>, CodecError> {
484        if let ExpandSpec::Exact(size) = self.spec {
485            if self.written != size {
486                return Err(CodecError::Malformed(format!(
487                    "expansion produced {} of declared exact {size} bytes",
488                    self.written
489                )));
490            }
491        }
492        let bytes = self.ctx.arena.alloc(self.buffer.into_boxed_slice());
493        let space = self.ctx.allocate_space();
494        Ok(View::over_space(bytes, space))
495    }
496
497    /// Returns how many bytes have been written so far.
498    pub fn written(&self) -> u64 {
499        self.written
500    }
501}