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