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