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;
5use std::io::SeekFrom;
6
7use crate::{CodecError, ReadSeek};
8
9use super::arena::DecodeArena;
10use super::budget::{DecodeBudget, DepthGuard};
11use super::depth::DecodeDepth;
12use super::error::{ResourceDimension, ResourceFailure, ResourceLimit};
13use super::policy::{
14    DecodePolicy, DECOMPRESSED_PER_EXPAND_BASE, DECOMPRESSED_PER_EXPAND_PER_INPUT_BYTE,
15};
16use super::space::{ByteRange, SpaceId};
17use super::view::{u64_from_index, View};
18
19#[derive(Clone, Copy)]
20enum LimitScope {
21    Global,
22    PerExpand,
23}
24
25/// Cap on the initial per-expand reservation before any output is produced.
26const RESERVE_CLAMP: usize = 8 * 1024 * 1024;
27
28/// Shared monotonic decode state.
29#[derive(Debug)]
30pub struct DecodeContext<'a> {
31    arena: &'a DecodeArena,
32    depth: DecodeDepth,
33    pub(super) budget: DecodeBudget,
34    derived_spaces: Cell<usize>,
35}
36
37impl<'a> DecodeContext<'a> {
38    /// Creates one session before input acquisition and detection.
39    pub fn new(arena: &'a DecodeArena, policy: &DecodePolicy, depth: DecodeDepth) -> Self {
40        Self {
41            arena,
42            depth,
43            budget: DecodeBudget::new(*policy, 0),
44            derived_spaces: Cell::new(0),
45        }
46    }
47
48    /// Starts post-decode processing with the measured byte length of an already
49    /// loaded input. This admits the input-proportional allowances without
50    /// keeping another copy of the source bytes. Format-declared counts must
51    /// not be used as the input length.
52    pub fn for_loaded_input(
53        arena: &'a DecodeArena,
54        policy: &DecodePolicy,
55        input_bytes: u64,
56    ) -> Result<Self, CodecError> {
57        let ctx = Self::new(arena, policy, DecodeDepth::Full);
58        ctx.budget
59            .charge_input(input_bytes, "admit loaded input length")?;
60        Ok(ctx)
61    }
62
63    /// Reads the root input under `max_input_bytes`, copies it into the arena,
64    /// registers the root space, establishes input-proportional allowances,
65    /// and returns the context and root view.
66    pub fn read_root(
67        reader: &mut dyn ReadSeek,
68        arena: &'a DecodeArena,
69        policy: &DecodePolicy,
70        depth: DecodeDepth,
71    ) -> Result<(Self, View<'a>), CodecError> {
72        if reader.seek(SeekFrom::End(0)).is_ok() {
73            reader.rewind().map_err(CodecError::Io)?;
74        }
75        let ctx = Self::new(arena, policy, depth);
76        let mut buffer = Vec::new();
77        ctx.complete_input(reader, &mut buffer)?;
78        let bytes = arena.alloc(buffer.into_boxed_slice());
79        Ok((ctx, View::over_space(bytes, SpaceId::ROOT)))
80    }
81
82    /// Builds a full-depth context over caller-owned root bytes, for fuzz
83    /// targets and tests. The arena still backs any expansions produced during
84    /// decode.
85    pub fn from_root_bytes(
86        bytes: &'a [u8],
87        arena: &'a DecodeArena,
88        policy: &DecodePolicy,
89    ) -> Result<(Self, View<'a>), CodecError> {
90        Self::from_root_bytes_at(bytes, arena, policy, DecodeDepth::Full)
91    }
92
93    /// Builds a context over caller-owned root bytes that requests `depth`.
94    pub fn from_root_bytes_at(
95        bytes: &'a [u8],
96        arena: &'a DecodeArena,
97        policy: &DecodePolicy,
98        depth: DecodeDepth,
99    ) -> Result<(Self, View<'a>), CodecError> {
100        Self::from_bytes_limit(bytes, arena, policy, depth).map_err(Into::into)
101    }
102
103    /// Builds a full-depth root context with a resource-only refusal channel.
104    pub fn from_root_bytes_limit(
105        bytes: &'a [u8],
106        arena: &'a DecodeArena,
107        policy: &DecodePolicy,
108    ) -> Result<(Self, View<'a>), ResourceLimit> {
109        Self::from_bytes_limit(bytes, arena, policy, DecodeDepth::Full)
110    }
111
112    fn from_bytes_limit(
113        bytes: &'a [u8],
114        arena: &'a DecodeArena,
115        policy: &DecodePolicy,
116        depth: DecodeDepth,
117    ) -> Result<(Self, View<'a>), ResourceLimit> {
118        let length = u64_from_index(bytes.len());
119        if length > policy.limits.max_input_bytes {
120            return Err(root_limit(
121                ResourceFailure::BudgetExceeded,
122                policy.limits.max_input_bytes,
123                length,
124            ));
125        }
126        let ctx = DecodeContext {
127            arena,
128            depth,
129            budget: DecodeBudget::new(*policy, length),
130            derived_spaces: Cell::new(0),
131        };
132        Ok((ctx, View::over_space(bytes, SpaceId::ROOT)))
133    }
134
135    /// Returns the decode policy in force.
136    pub fn policy(&self) -> &DecodePolicy {
137        self.budget.policy()
138    }
139
140    /// Returns the depth the caller requested.
141    pub fn depth(&self) -> DecodeDepth {
142        self.depth
143    }
144
145    /// Returns whether the caller requested only the container layer.
146    pub fn container_only(&self) -> bool {
147        self.depth == DecodeDepth::Container
148    }
149
150    fn decompression_allowance(&self) -> u64 {
151        self.budget.decompression_allowance()
152    }
153
154    /// The largest size one expansion may declare: the smaller of
155    /// `max_decompressed_bytes_per_expand` and its input-proportional
156    /// allowance.
157    ///
158    /// A codec compares a declared expansion size against it before
159    /// [`DecodeContext::begin_expand`], so a declared size above it is a
160    /// per-input bound that skips one member instead of fusing the session.
161    #[must_use]
162    pub fn per_expand_allowance(&self) -> u64 {
163        self.budget.proportional_allowance(
164            self.budget
165                .policy()
166                .limits
167                .max_decompressed_bytes_per_expand,
168            DECOMPRESSED_PER_EXPAND_BASE,
169            DECOMPRESSED_PER_EXPAND_PER_INPUT_BYTE,
170        )
171    }
172
173    /// Charges bytes that decoded output keeps for the rest of the session.
174    ///
175    /// This is the one retained-bytes chokepoint: native record text, arena
176    /// buffers, and retained copies of source bytes call it. Temporary and
177    /// derived storage is not charged.
178    pub fn retain_bytes(&self, bytes: u64, operation: &'static str) -> Result<(), CodecError> {
179        self.budget
180            .retain_bytes(bytes, operation)
181            .map_err(Into::into)
182    }
183
184    /// Retained bytes the session can still afford: the retained-byte
185    /// allowance (the smaller of `max_retained_bytes` and the
186    /// input-proportional allowance) minus the bytes already retained, or zero
187    /// after a refusal fused the session.
188    ///
189    /// A codec uses this only to decide whether to skip optional enrichment
190    /// before it builds the enrichment, and reports the skip as a loss. It
191    /// does not reserve anything: the bytes the codec keeps are still charged
192    /// through [`DecodeContext::retain_bytes`], which can still refuse.
193    #[must_use]
194    pub fn retained_bytes_available(&self) -> u64 {
195        self.budget.retained_bytes_available()
196    }
197
198    fn allocate_space(&self) -> Result<SpaceId, CodecError> {
199        let used = self.derived_spaces.get();
200        // Root owns zero; every other `usize` value identifies a derived space.
201        let index = used.checked_add(1).ok_or_else(|| {
202            CodecError::NotImplemented("decode address spaces are exhausted".into())
203        })?;
204        self.derived_spaces.set(index);
205        Ok(SpaceId::from_index(index))
206    }
207
208    /// Records a permanent fuse and returns the resource error to propagate.
209    fn fuse(
210        &self,
211        reason: ResourceFailure,
212        scope: LimitScope,
213        amount: u64,
214        operation: &'static str,
215    ) -> CodecError {
216        let limit = match scope {
217            LimitScope::Global => self.decompression_allowance(),
218            LimitScope::PerExpand => self.per_expand_allowance(),
219        };
220        self.budget.refuse(
221            ResourceDimension::DecompressedBytes,
222            reason,
223            limit,
224            self.budget.decompressed_used(),
225            amount,
226            operation,
227        )
228    }
229
230    /// Allocates `count` copies of `value` with fallible allocation.
231    ///
232    /// Use this instead of `vec![value; parsed_count]` for a count read from
233    /// the input; validate the count against the bytes it describes first
234    /// (`View::counted`, `bounded_len`).
235    pub fn alloc_filled<T: Clone>(
236        &self,
237        count: usize,
238        value: T,
239        operation: &'static str,
240    ) -> Result<Vec<T>, CodecError> {
241        let mut values = Vec::new();
242        values.try_reserve_exact(count).map_err(|_| {
243            self.budget
244                .retained_allocation_failed(u64_from_index(count), operation)
245        })?;
246        values.resize(count, value);
247        Ok(values)
248    }
249
250    /// Charges admitted entities.
251    pub fn charge_entities(&self, count: u64, operation: &'static str) -> Result<(), CodecError> {
252        self.budget.charge_entities(count, operation)
253    }
254
255    /// Charges entities newly present since `admitted`, then advances `admitted`.
256    ///
257    /// Codecs call this at admission boundaries so `max_entities` refuses further
258    /// work instead of only reporting after a finished IR is built.
259    pub fn admit_entities(
260        &self,
261        current: u64,
262        admitted: &mut u64,
263        operation: &'static str,
264    ) -> Result<(), CodecError> {
265        if current < *admitted {
266            *admitted = current;
267            return Ok(());
268        }
269        let additional = current - *admitted;
270        self.charge_entities(additional, operation)?;
271        *admitted = current;
272        Ok(())
273    }
274
275    /// Enters one recursive nesting level until the returned guard is dropped.
276    pub fn enter_nested(&self, operation: &'static str) -> Result<DepthGuard<'_>, CodecError> {
277        self.enter_nested_limit(operation).map_err(Into::into)
278    }
279
280    /// Enters one nesting level with a resource-only refusal.
281    pub fn enter_nested_limit(
282        &self,
283        operation: &'static str,
284    ) -> Result<DepthGuard<'_>, ResourceLimit> {
285        self.budget.enter_nested(operation)
286    }
287
288    /// Returns the resource refusal that has fused this decode session.
289    pub fn resource_refusal(&self) -> Option<ResourceLimit> {
290        self.budget.fused()
291    }
292
293    // --- decompression ------------------------------------------------------
294
295    /// Begins an expansion whose output is charged incrementally and becomes
296    /// available only after successful finalization.
297    pub fn begin_expand(&self, spec: ExpandSpec) -> Result<ExpandWriter<'_, 'a>, CodecError> {
298        if let Some(limit) = self.budget.fused() {
299            return Err(CodecError::ResourceLimit(limit));
300        }
301        if let ExpandSpec::Exact(size) = spec {
302            let per_expand = self.per_expand_allowance();
303            if size > per_expand {
304                return Err(self.fuse(
305                    ResourceFailure::BudgetExceeded,
306                    LimitScope::PerExpand,
307                    size,
308                    "begin_expand",
309                ));
310            }
311            if self
312                .decompression_allowance()
313                .checked_sub(self.budget.decompressed_used())
314                .is_none_or(|remaining| size > remaining)
315            {
316                return Err(self.fuse(
317                    ResourceFailure::BudgetExceeded,
318                    LimitScope::Global,
319                    size,
320                    "begin_expand",
321                ));
322            }
323        }
324        let mut buffer: Vec<u8> = Vec::new();
325        let reserve = match spec {
326            // A declared size the address space cannot name is above the cap,
327            // so the cap is the reservation either way.
328            ExpandSpec::Exact(size) => match usize::try_from(size) {
329                Ok(size) => size.min(RESERVE_CLAMP),
330                Err(_) => RESERVE_CLAMP,
331            },
332            ExpandSpec::Unknown => 0,
333        };
334        if reserve > 0 {
335            buffer.try_reserve(reserve).map_err(|_| {
336                self.fuse(
337                    ResourceFailure::AllocationFailed,
338                    LimitScope::PerExpand,
339                    u64_from_index(reserve),
340                    "begin_expand",
341                )
342            })?;
343        }
344        Ok(ExpandWriter {
345            ctx: self,
346            spec,
347            buffer,
348        })
349    }
350
351    /// Copies several input extents into one derived view.
352    pub fn concat_views(&self, inputs: &[View<'_>]) -> Result<View<'a>, CodecError> {
353        if let Some(limit) = self.budget.fused() {
354            return Err(CodecError::ResourceLimit(limit));
355        }
356        if inputs.is_empty() {
357            return Err(CodecError::Malformed(
358                "cannot concatenate an empty view list".into(),
359            ));
360        }
361        let total = inputs.iter().try_fold(0usize, |total, view| {
362            total.checked_add(view.window().len()).ok_or_else(|| {
363                self.budget.refuse(
364                    ResourceDimension::RetainedBytes,
365                    ResourceFailure::BudgetExceeded,
366                    self.budget.policy().limits.max_retained_bytes,
367                    u64_from_index(total),
368                    u64_from_index(view.window().len()),
369                    "concat_views",
370                )
371            })
372        })?;
373        self.retain_bytes(u64_from_index(total), "concat_views")?;
374        let mut buffer = Vec::new();
375        buffer.try_reserve_exact(total).map_err(|_| {
376            self.budget
377                .retained_allocation_failed(u64_from_index(total), "concat_views")
378        })?;
379        for view in inputs {
380            buffer.extend_from_slice(view.window());
381        }
382        let bytes = self.arena.alloc(buffer.into_boxed_slice());
383        let space = self.allocate_space()?;
384        Ok(View::over_space(bytes, space))
385    }
386
387    /// Concatenates owned chunks into one retained buffer without an arena copy.
388    pub fn concat_retained(
389        &self,
390        inputs: &[Vec<u8>],
391        operation: &'static str,
392    ) -> Result<Vec<u8>, CodecError> {
393        if let Some(limit) = self.budget.fused() {
394            return Err(CodecError::ResourceLimit(limit));
395        }
396        if inputs.is_empty() {
397            return Err(CodecError::Malformed(
398                "cannot concatenate an empty buffer list".into(),
399            ));
400        }
401        let total = inputs.iter().try_fold(0_usize, |total, input| {
402            total.checked_add(input.len()).ok_or_else(|| {
403                CodecError::NotImplemented("retained concatenation exceeds usize".into())
404            })
405        })?;
406        let total_bytes = crate::decode::u64_from_index(total);
407        self.retain_bytes(total_bytes, operation)?;
408        let mut buffer = Vec::new();
409        buffer.try_reserve_exact(total).map_err(|_| {
410            self.budget
411                .retained_allocation_failed(total_bytes, operation)
412        })?;
413        for input in inputs {
414            buffer.extend_from_slice(input);
415        }
416        Ok(buffer)
417    }
418
419    /// Registers a stored (uncompressed) child range as a space that borrows
420    /// the parent bytes without copying.
421    ///
422    /// `range` is expressed in the parent view's own space coordinates and must
423    /// lie within the parent window; a range that escapes the parent is refused
424    /// here, at the request site, exactly as [`View::child`] refuses. No bytes
425    /// are copied. It is the archive-entry counterpart of [`DecodeContext::begin_expand`] —
426    /// stored ZIP entries take this path, compressed ones take the expander.
427    /// Registration still refuses on a fused context so a stored entry cannot be
428    /// admitted after a refusal.
429    pub fn register_slice<'v>(
430        &self,
431        parent: View<'v>,
432        range: ByteRange,
433    ) -> Result<View<'v>, CodecError> {
434        if let Some(limit) = self.budget.fused() {
435            return Err(CodecError::ResourceLimit(limit));
436        }
437        let start = usize::try_from(range.start).ok();
438        let end = usize::try_from(range.end).ok();
439        let child = start
440            .zip(end)
441            .and_then(|(start, end)| parent.child(start, end))
442            .ok_or_else(|| {
443                CodecError::malformed(format_args!(
444                    "stored slice [{}, {}) escapes parent space {}",
445                    range.start,
446                    range.end,
447                    parent.space().index()
448                ))
449            })?;
450
451        let space = self.allocate_space()?;
452        Ok(View::over_space(child.window(), space))
453    }
454
455    // --- lifecycle ----------------------------------------------------------
456
457    /// Closes a decode or inspection session, returning a fused resource error
458    /// even when codec code swallowed the charge that caused it.
459    pub fn finish_session(self) -> Result<(), CodecError> {
460        if let Some(limit) = self.budget.fused() {
461            return Err(CodecError::ResourceLimit(limit));
462        }
463        Ok(())
464    }
465}
466
467fn root_limit(reason: ResourceFailure, limit: u64, used: u64) -> ResourceLimit {
468    if used <= limit {
469        return ResourceLimit {
470            dimension: ResourceDimension::InputBytes,
471            reason,
472            limit,
473            used,
474            additional: 0,
475            operation: "read_root",
476        };
477    }
478    let additional = used - limit;
479    ResourceLimit {
480        dimension: ResourceDimension::InputBytes,
481        reason,
482        limit,
483        used,
484        additional,
485        operation: "read_root",
486    }
487}
488
489/// How much output an expansion is expected to produce.
490#[derive(Debug, Clone, Copy, PartialEq, Eq)]
491pub enum ExpandSpec {
492    /// A declared exact size, enforced per-write and at finalize.
493    Exact(u64),
494    /// No trustworthy declared size: the decompression limits apply.
495    Unknown,
496}
497
498/// Writes decompressed output under incremental charging.
499#[derive(Debug)]
500pub struct ExpandWriter<'ctx, 'a> {
501    ctx: &'ctx DecodeContext<'a>,
502    spec: ExpandSpec,
503    buffer: Vec<u8>,
504}
505
506impl<'a> ExpandWriter<'_, 'a> {
507    /// Appends decompressed output, charging before it is retained.
508    pub fn write(&mut self, data: &[u8]) -> Result<(), CodecError> {
509        let len = u64_from_index(data.len());
510        let new_written = self.written().checked_add(len).ok_or_else(|| {
511            self.ctx.fuse(
512                ResourceFailure::BudgetExceeded,
513                LimitScope::PerExpand,
514                len,
515                "expand_write",
516            )
517        })?;
518        match self.spec {
519            ExpandSpec::Exact(size) if new_written > size => {
520                return Err(CodecError::malformed(format_args!(
521                    "expansion exceeded declared exact size {size}"
522                )))
523            }
524            _ => {}
525        }
526        let per_expand = self.ctx.per_expand_allowance();
527        if new_written > per_expand {
528            return Err(self.ctx.fuse(
529                ResourceFailure::BudgetExceeded,
530                LimitScope::PerExpand,
531                len,
532                "expand_write",
533            ));
534        }
535        self.ctx.budget.charge_decompressed(len, "expand_write")?;
536        self.buffer.try_reserve(data.len()).map_err(|_| {
537            self.ctx.fuse(
538                ResourceFailure::AllocationFailed,
539                LimitScope::PerExpand,
540                len,
541                "expand_write",
542            )
543        })?;
544        self.buffer.extend_from_slice(data);
545        Ok(())
546    }
547
548    /// Finalizes the expansion, stores it in the arena, and registers its space.
549    pub fn finalize(self) -> Result<View<'a>, CodecError> {
550        self.check_exact()?;
551        let bytes = self.ctx.arena.alloc(self.buffer.into_boxed_slice());
552        let space = self.ctx.allocate_space()?;
553        Ok(View::over_space(bytes, space))
554    }
555
556    /// Finalizes an expansion directly into a caller-owned buffer.
557    /// The expansion budget already admits its bytes, so no retained copy occurs.
558    pub fn finalize_owned(self) -> Result<Vec<u8>, CodecError> {
559        self.check_exact()?;
560        Ok(self.buffer)
561    }
562
563    fn check_exact(&self) -> Result<(), CodecError> {
564        if let ExpandSpec::Exact(size) = self.spec {
565            if self.written() != size {
566                return Err(CodecError::malformed(format_args!(
567                    "expansion produced {} of declared exact {size} bytes",
568                    self.written()
569                )));
570            }
571        }
572        Ok(())
573    }
574
575    /// Returns how many bytes have been written so far.
576    pub fn written(&self) -> u64 {
577        u64_from_index(self.buffer.len())
578    }
579}
580
581#[cfg(test)]
582mod tests {
583    use super::{ByteRange, DecodeArena, DecodeContext, DecodeDepth, DecodePolicy};
584
585    use std::io::{self, Cursor, Read, Seek, SeekFrom};
586
587    struct RewindFails(Cursor<Vec<u8>>);
588
589    impl Read for RewindFails {
590        fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
591            self.0.read(buffer)
592        }
593    }
594
595    impl Seek for RewindFails {
596        fn seek(&mut self, position: SeekFrom) -> io::Result<u64> {
597            if position == SeekFrom::Start(0) {
598                Err(io::Error::new(
599                    io::ErrorKind::PermissionDenied,
600                    "rewind denied",
601                ))
602            } else {
603                self.0.seek(position)
604            }
605        }
606    }
607
608    #[test]
609    fn root_reader_preserves_bytes_across_fixed_chunk_boundaries() {
610        let mut bytes = vec![0x5a_u8; 8193];
611        bytes[8192] = 0x7f;
612        let mut reader = Cursor::new(bytes.clone());
613        let arena = DecodeArena::new();
614        let (_, root) = DecodeContext::read_root(
615            &mut reader,
616            &arena,
617            &DecodePolicy::default(),
618            DecodeDepth::Full,
619        )
620        .expect("root is admitted");
621        assert_eq!(root.window(), bytes.as_slice());
622    }
623
624    #[test]
625    fn root_reader_propagates_failed_rewind_after_a_successful_size_probe() {
626        let arena = DecodeArena::new();
627        let mut reader = RewindFails(Cursor::new(b"not empty".to_vec()));
628        assert!(matches!(
629            DecodeContext::read_root(
630                &mut reader,
631                &arena,
632                &DecodePolicy::default(),
633                DecodeDepth::Full
634            ),
635            Err(crate::CodecError::Io(error)) if error.kind() == io::ErrorKind::PermissionDenied
636        ));
637    }
638
639    #[test]
640    fn exhausted_space_ids_refuse_registration_without_reusing_an_id() {
641        let arena = DecodeArena::new();
642        let (ctx, root) = DecodeContext::from_root_bytes(b"x", &arena, &DecodePolicy::default())
643            .expect("test input fits the policy");
644        ctx.derived_spaces.set(usize::MAX - 1);
645        let range = ByteRange { start: 0, end: 1 };
646        let view = ctx
647            .register_slice(root, range)
648            .expect("maximum space id is allocatable");
649        assert_eq!(view.space().index(), usize::MAX);
650        let error = ctx
651            .register_slice(root, range)
652            .expect_err("space IDs are exhausted");
653        assert!(matches!(error, crate::CodecError::NotImplemented(_)));
654        assert!(ctx.register_slice(root, range).is_err());
655    }
656}