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, ScopedReservation, WorkBudget};
11use super::error::{ResourceDimension, ResourceFailure, ResourceLimit};
12use super::policy::{
13    DecodePolicy, DECOMPRESSED_PER_EXPAND_BASE, DECOMPRESSED_PER_EXPAND_PER_INPUT_BYTE,
14};
15use super::space::{ByteRange, SpaceId};
16use super::view::{u64_from_index, View};
17
18#[derive(Clone, Copy)]
19enum LimitScope {
20    Global,
21    PerExpand,
22}
23
24/// Cap on the initial per-expand reservation before any output is produced.
25const RESERVE_CLAMP: usize = 8 * 1024 * 1024;
26
27/// Shared monotonic decode state.
28#[derive(Debug)]
29pub struct DecodeContext<'a> {
30    arena: &'a DecodeArena,
31    container_only: bool,
32    pub(super) budget: DecodeBudget,
33    derived_spaces: Cell<usize>,
34}
35
36impl<'a> DecodeContext<'a> {
37    /// Creates one session before input acquisition and detection.
38    pub fn new(arena: &'a DecodeArena, policy: &DecodePolicy, container_only: bool) -> Self {
39        Self {
40            arena,
41            container_only,
42            budget: DecodeBudget::new(*policy, 0),
43            derived_spaces: Cell::new(0),
44        }
45    }
46
47    /// Starts post-decode processing with the measured byte length of an already
48    /// loaded input. This admits the input-proportional allowances without
49    /// keeping another copy of the source bytes. Format-declared counts must
50    /// not be used as the input length.
51    pub fn for_loaded_input(
52        arena: &'a DecodeArena,
53        policy: &DecodePolicy,
54        input_bytes: u64,
55    ) -> Result<Self, CodecError> {
56        let ctx = Self::new(arena, policy, false);
57        ctx.budget
58            .charge_input(input_bytes, "admit loaded input length")?;
59        Ok(ctx)
60    }
61
62    /// Reads the root input under `max_input_bytes`, copies it into the arena,
63    /// registers the root space, establishes input-proportional allowances,
64    /// and returns the context and root view.
65    pub fn read_root(
66        reader: &mut dyn ReadSeek,
67        arena: &'a DecodeArena,
68        policy: &DecodePolicy,
69        container_only: bool,
70    ) -> Result<(Self, View<'a>), CodecError> {
71        if reader.seek(SeekFrom::End(0)).is_ok() {
72            reader.rewind().map_err(CodecError::Io)?;
73        }
74        let ctx = Self::new(arena, policy, container_only);
75        let mut buffer = Vec::new();
76        ctx.complete_input(reader, &mut buffer)?;
77        let bytes = arena.alloc(&ctx, buffer.into_boxed_slice())?;
78        Ok((ctx, View::over_space(bytes, SpaceId::ROOT)))
79    }
80
81    /// Builds a context over caller-owned root bytes, for fuzz targets and
82    /// tests. The arena still backs any expansions produced during decode.
83    pub fn from_root_bytes(
84        bytes: &'a [u8],
85        arena: &'a DecodeArena,
86        policy: &DecodePolicy,
87    ) -> Result<(Self, View<'a>), CodecError> {
88        Self::from_bytes(bytes, arena, policy, false)
89    }
90
91    /// Builds a root context with a resource-only refusal channel.
92    pub fn from_root_bytes_limit(
93        bytes: &'a [u8],
94        arena: &'a DecodeArena,
95        policy: &DecodePolicy,
96    ) -> Result<(Self, View<'a>), ResourceLimit> {
97        Self::from_bytes_limit(bytes, arena, policy, false)
98    }
99
100    fn from_bytes(
101        bytes: &'a [u8],
102        arena: &'a DecodeArena,
103        policy: &DecodePolicy,
104        container_only: bool,
105    ) -> Result<(Self, View<'a>), CodecError> {
106        Self::from_bytes_limit(bytes, arena, policy, container_only).map_err(Into::into)
107    }
108
109    fn from_bytes_limit(
110        bytes: &'a [u8],
111        arena: &'a DecodeArena,
112        policy: &DecodePolicy,
113        container_only: bool,
114    ) -> Result<(Self, View<'a>), ResourceLimit> {
115        let length = u64_from_index(bytes.len());
116        if length > policy.limits.max_input_bytes {
117            return Err(root_limit(
118                ResourceFailure::BudgetExceeded,
119                policy.limits.max_input_bytes,
120                length,
121            ));
122        }
123        let ctx = DecodeContext {
124            arena,
125            container_only,
126            budget: DecodeBudget::new(*policy, length),
127            derived_spaces: Cell::new(0),
128        };
129        Ok((ctx, View::over_space(bytes, SpaceId::ROOT)))
130    }
131
132    /// Returns the decode policy in force.
133    pub fn policy(&self) -> &DecodePolicy {
134        self.budget.policy()
135    }
136
137    /// Returns whether the caller requested container-only decoding.
138    pub fn container_only(&self) -> bool {
139        self.container_only
140    }
141
142    fn decompression_allowance(&self) -> u64 {
143        self.budget.decompression_allowance()
144    }
145
146    fn per_expand_allowance(&self) -> u64 {
147        let policy_limit = self
148            .budget
149            .policy()
150            .limits
151            .max_decompressed_bytes_per_expand;
152        let Some(proportional) = DECOMPRESSED_PER_EXPAND_PER_INPUT_BYTE
153            .checked_mul(self.budget.input_bytes())
154            .and_then(|bytes| DECOMPRESSED_PER_EXPAND_BASE.checked_add(bytes))
155        else {
156            return policy_limit;
157        };
158        policy_limit.min(proportional)
159    }
160
161    fn allocate_space(&self) -> Result<SpaceId, CodecError> {
162        let used = self.derived_spaces.get();
163        let index = used.checked_add(1).ok_or_else(|| {
164            // Root owns zero; every other `usize` value identifies a derived space.
165            self.budget.refuse(
166                ResourceDimension::Codec("decode address spaces"),
167                ResourceFailure::BudgetExceeded,
168                u64_from_index(usize::MAX),
169                u64_from_index(used),
170                1,
171                "decode address spaces",
172            )
173        })?;
174        self.derived_spaces.set(index);
175        Ok(SpaceId::from_index(index))
176    }
177
178    /// Records a permanent fuse and returns the resource error to propagate.
179    fn fuse(
180        &self,
181        reason: ResourceFailure,
182        scope: LimitScope,
183        amount: u64,
184        operation: &'static str,
185    ) -> CodecError {
186        let limit = match scope {
187            LimitScope::Global => self.decompression_allowance(),
188            LimitScope::PerExpand => self.per_expand_allowance(),
189        };
190        self.budget.refuse(
191            ResourceDimension::DecompressedBytes,
192            reason,
193            limit,
194            self.budget.decompressed_used(),
195            amount,
196            operation,
197        )
198    }
199
200    /// Reserves bytes held by a temporary materialization.
201    pub fn reserve_scoped(
202        &self,
203        bytes: u64,
204        operation: &'static str,
205    ) -> Result<ScopedReservation<'_>, CodecError> {
206        self.budget.reserve_scoped(bytes, operation)
207    }
208
209    /// Admits a temporary byte allocation and holds its reservation until released.
210    pub fn try_materialized<T>(
211        &self,
212        bytes: u64,
213        operation: &'static str,
214        allocate: impl FnOnce() -> Result<T, std::collections::TryReserveError>,
215    ) -> Result<(ScopedReservation<'_>, T), CodecError> {
216        let reservation = self.reserve_scoped(bytes, operation)?;
217        let value =
218            allocate().map_err(|_| self.budget.scoped_allocation_failed(bytes, operation))?;
219        Ok((reservation, value))
220    }
221
222    /// Reserves temporary bytes and returns the typed resource refusal.
223    pub fn reserve_scoped_limit(
224        &self,
225        bytes: u64,
226        operation: &'static str,
227    ) -> Result<ScopedReservation<'_>, ResourceLimit> {
228        self.budget.reserve_scoped_limit(bytes, operation)
229    }
230
231    /// Charges bytes retained for the remainder of this session.
232    pub fn charge_retained(&self, bytes: u64, operation: &'static str) -> Result<(), CodecError> {
233        self.budget.charge_retained(bytes, operation)
234    }
235
236    pub(crate) fn retained_size_overflow_limit(&self, operation: &'static str) -> ResourceLimit {
237        self.budget.retained_size_overflow_limit(operation)
238    }
239
240    /// Charge retained storage with a resource-only refusal channel.
241    pub fn charge_retained_limit(
242        &self,
243        bytes: u64,
244        operation: &'static str,
245    ) -> Result<(), ResourceLimit> {
246        self.budget.charge_retained_limit(bytes, operation)
247    }
248
249    /// Charges storage created by the closure as temporary until its reservation is dropped.
250    pub fn with_scoped_storage<T, E: From<CodecError>>(
251        &self,
252        operation: &'static str,
253        build: impl FnOnce() -> Result<T, E>,
254    ) -> Result<(T, ScopedReservation<'_>), E> {
255        let mut storage = self.reserve_scoped(0, operation)?;
256        let value = storage.with_storage(build)?;
257        Ok((value, storage))
258    }
259
260    /// Copies admitted text into retained storage with a typed resource refusal.
261    pub fn copy_retained_text_limit(
262        &self,
263        text: &str,
264        operation: &'static str,
265    ) -> Result<String, ResourceLimit> {
266        let bytes = super::u64_from_index(text.len());
267        self.budget.charge_work_limit(bytes, operation)?;
268        self.budget.charge_retained_limit(bytes, operation)?;
269        let mut copy = String::new();
270        copy.try_reserve_exact(text.len()).map_err(|_| {
271            self.budget
272                .retained_allocation_failed_limit(bytes, operation)
273        })?;
274        copy.push_str(text);
275        Ok(copy)
276    }
277
278    /// Copies admitted text into session-retained storage.
279    pub fn copy_retained_text(
280        &self,
281        text: &str,
282        operation: &'static str,
283    ) -> Result<String, CodecError> {
284        self.copy_retained_text_limit(text, operation)
285            .map_err(Into::into)
286    }
287
288    /// Copies bytes into session-retained storage after charging and reserving safely.
289    pub fn copy_retained(
290        &self,
291        bytes: &[u8],
292        operation: &'static str,
293    ) -> Result<Vec<u8>, CodecError> {
294        self.charge_work(u64_from_index(bytes.len()), operation)?;
295        self.charge_retained(u64_from_index(bytes.len()), operation)?;
296        let mut copy = Vec::new();
297        copy.try_reserve_exact(bytes.len()).map_err(|_| {
298            self.budget
299                .retained_allocation_failed(u64_from_index(bytes.len()), operation)
300        })?;
301        copy.extend_from_slice(bytes);
302        Ok(copy)
303    }
304
305    /// Reserves growth of a session-retained string after charging its bytes.
306    pub fn try_reserve_retained_text(
307        &self,
308        text: &mut String,
309        additional: usize,
310        operation: &'static str,
311    ) -> Result<(), CodecError> {
312        let required = text
313            .len()
314            .checked_add(additional)
315            .ok_or_else(|| CodecError::from(self.retained_size_overflow_limit(operation)))?;
316        let growth = if required > text.capacity() {
317            required - text.capacity()
318        } else {
319            0
320        };
321        let bytes = u64_from_index(growth);
322        self.charge_retained(bytes, operation)?;
323        Self::reserve_admitted_string(text, additional, operation)
324            .map_err(|_| self.budget.retained_allocation_failed(bytes, operation))
325    }
326
327    /// Copies source bytes as UTF-8 with replacement characters after charging
328    /// the exact length of the retained text.
329    pub fn copy_retained_lossy_utf8(
330        &self,
331        value: &[u8],
332        operation: &'static str,
333    ) -> Result<String, CodecError> {
334        self.charge_work(u64_from_index(value.len()), operation)?;
335        let mut remaining = value;
336        let mut length = 0usize;
337        loop {
338            match std::str::from_utf8(remaining) {
339                Ok(valid) => {
340                    length = length
341                        .checked_add(valid.len())
342                        .ok_or_else(|| self.refuse_codec_limit(operation, u64::MAX, u64::MAX))?;
343                    break;
344                }
345                Err(error) => {
346                    length = length
347                        .checked_add(error.valid_up_to())
348                        .and_then(|length| length.checked_add('\u{FFFD}'.len_utf8()))
349                        .ok_or_else(|| self.refuse_codec_limit(operation, u64::MAX, u64::MAX))?;
350                    let Some(invalid_len) = error.error_len() else {
351                        break;
352                    };
353                    remaining = &remaining[error.valid_up_to() + invalid_len..];
354                }
355            }
356        }
357        let mut text = String::new();
358        self.charge_work(u64_from_index(value.len()), operation)?;
359        self.charge_work(u64_from_index(length), operation)?;
360        self.try_reserve_retained_text(&mut text, length, operation)?;
361        let mut remaining = value;
362        loop {
363            match std::str::from_utf8(remaining) {
364                Ok(valid) => {
365                    text.push_str(valid);
366                    break;
367                }
368                Err(error) => {
369                    let valid_len = error.valid_up_to();
370                    self.charge_work(u64_from_index(valid_len), operation)?;
371                    text.push_str(
372                        std::str::from_utf8(&remaining[..valid_len])
373                            .map_err(|_| CodecError::malformed("valid UTF-8 prefix changed"))?,
374                    );
375                    text.push('\u{FFFD}');
376                    let Some(invalid_len) = error.error_len() else {
377                        break;
378                    };
379                    remaining = &remaining[valid_len + invalid_len..];
380                }
381            }
382        }
383        Ok(text)
384    }
385
386    /// Allocates `count` copies of `value` after charging slots, backing bytes
387    /// and fill work, with fallible storage admission.
388    ///
389    /// Prefer this over `vec![value; parsed_count]` for attacker-influenced sizes.
390    pub fn alloc_filled<T: Clone>(
391        &self,
392        count: usize,
393        value: T,
394        operation: &'static str,
395    ) -> Result<Vec<T>, CodecError> {
396        self.charge_work(u64_from_index(count), operation)?;
397        let mut values = self.collection_vec(count, operation)?;
398        values.resize(count, value);
399        Ok(values)
400    }
401
402    /// Allocates `count` copies of `value` whose backing bytes the caller has
403    /// already admitted, as retained or scoped storage, under its own operation.
404    ///
405    /// Charges collection items and fill work only, so the bytes are admitted
406    /// exactly once. The caller must have admitted `count * size_of::<T>()`
407    /// bytes before this call.
408    pub fn alloc_filled_admitted<T: Clone>(
409        &self,
410        count: usize,
411        value: T,
412        operation: &'static str,
413    ) -> Result<Vec<T>, CodecError> {
414        self.charge_work(u64_from_index(count), operation)?;
415        self.charge_collection_items(u64_from_index(count), operation)?;
416        let mut values = Vec::new();
417        values
418            .try_reserve_exact(count)
419            .map_err(|_| self.collection_allocation_failed(count, operation))?;
420        values.resize(count, value);
421        Ok(values)
422    }
423
424    /// Charges admitted entities.
425    pub fn charge_entities(&self, count: u64, operation: &'static str) -> Result<(), CodecError> {
426        self.budget.charge_entities(count, operation)
427    }
428
429    /// Charges entities newly present since `admitted`, then advances `admitted`.
430    ///
431    /// Codecs call this at admission boundaries so `max_entities` refuses further
432    /// work instead of only reporting after a finished IR is built.
433    pub fn admit_entities(
434        &self,
435        current: u64,
436        admitted: &mut u64,
437        operation: &'static str,
438    ) -> Result<(), CodecError> {
439        if current < *admitted {
440            *admitted = current;
441            return Ok(());
442        }
443        let additional = current - *admitted;
444        self.charge_entities(additional, operation)?;
445        *admitted = current;
446        Ok(())
447    }
448
449    /// Charges admitted collection items.
450    pub fn charge_collection_items(
451        &self,
452        count: u64,
453        operation: &'static str,
454    ) -> Result<(), CodecError> {
455        self.budget.charge_collection_items(count, operation)
456    }
457
458    /// Charges collection items before a fallible collection allocation.
459    pub fn try_collection<T>(
460        &self,
461        count: usize,
462        operation: &'static str,
463        allocate: impl FnOnce() -> Result<T, std::collections::TryReserveError>,
464    ) -> Result<T, CodecError> {
465        let count = u64_from_index(count);
466        self.charge_collection_items(count, operation)?;
467        allocate().map_err(|_| self.budget.collection_allocation_failed(count, operation))
468    }
469
470    /// Reports allocator refusal after a collection-item charge was recorded.
471    pub(super) fn collection_allocation_failed(
472        &self,
473        count: usize,
474        operation: &'static str,
475    ) -> CodecError {
476        self.budget
477            .collection_allocation_failed(u64_from_index(count), operation)
478    }
479
480    /// Charges collection items and returns the typed refusal for resource-only callers.
481    pub fn charge_collection_items_limit(
482        &self,
483        count: u64,
484        operation: &'static str,
485    ) -> Result<(), ResourceLimit> {
486        self.budget.charge_collection_items_limit(count, operation)
487    }
488
489    /// Enters one recursive nesting level until the returned guard is dropped.
490    pub fn enter_nested(&self, operation: &'static str) -> Result<DepthGuard<'_>, CodecError> {
491        self.enter_nested_limit(operation).map_err(Into::into)
492    }
493
494    /// Enters one nesting level with a resource-only refusal.
495    pub fn enter_nested_limit(
496        &self,
497        operation: &'static str,
498    ) -> Result<DepthGuard<'_>, ResourceLimit> {
499        self.budget.enter_nested(operation)
500    }
501
502    /// Charges session-global algorithm work, fusing on refusal.
503    pub fn charge_work(&self, units: u64, operation: &'static str) -> Result<(), CodecError> {
504        self.budget.charge_work(units, operation)
505    }
506
507    /// Charges work with a resource-only refusal channel.
508    pub fn charge_work_limit(
509        &self,
510        units: u64,
511        operation: &'static str,
512    ) -> Result<(), ResourceLimit> {
513        self.budget.charge_work_limit(units, operation)
514    }
515
516    /// Sort admitted values stably using fallible index scratch.
517    ///
518    /// `key_bytes` states the key bytes a comparison can read from each value.
519    /// Equal values retain input order. Values move in place without cloning children.
520    pub fn stable_sort_by<T>(
521        &self,
522        values: &mut [T],
523        mut compare: impl FnMut(&T, &T) -> std::cmp::Ordering,
524        key_bytes: impl Fn(&T) -> usize,
525        operation: &'static str,
526    ) -> Result<(), CodecError> {
527        let count = super::u64_from_index(values.len());
528        self.charge_work(count, operation)?;
529        let mut compared = |left: usize, right: usize| {
530            let work = super::u64_from_index(key_bytes(&values[left]))
531                .checked_add(super::u64_from_index(key_bytes(&values[right])))
532                .and_then(|work| work.checked_add(1))
533                .ok_or_else(|| self.refuse_codec_limit(operation, u64::MAX - 1, u64::MAX))?;
534            self.charge_work(work, operation)?;
535            Ok::<_, CodecError>(compare(&values[left], &values[right]))
536        };
537        let mut ordered = true;
538        for index in 1..values.len() {
539            if compared(index - 1, index)?.is_gt() {
540                ordered = false;
541                break;
542            }
543        }
544        if ordered {
545            return Ok(());
546        }
547        let index_bytes = super::u64_from_index(std::mem::size_of::<usize>());
548        let scratch_bytes = count
549            .checked_mul(index_bytes)
550            .and_then(|bytes| bytes.checked_mul(2))
551            .ok_or_else(|| self.refuse_codec_limit(operation, u64::MAX - 1, u64::MAX))?;
552        self.charge_work(scratch_bytes, operation)?;
553        let mut order_vec = Vec::new();
554        let mut destinations_vec = Vec::new();
555        let _scratch = if values.len() > super::sort::INLINE_SORT_LIMIT {
556            self.charge_collection_items(
557                count
558                    .checked_mul(2)
559                    .ok_or_else(|| self.refuse_codec_limit(operation, u64::MAX, u64::MAX))?,
560                operation,
561            )?;
562            let reservation = self.reserve_scoped(scratch_bytes, operation)?;
563            order_vec.try_reserve_exact(values.len()).map_err(|_| {
564                self.budget
565                    .scoped_allocation_failed(scratch_bytes, operation)
566            })?;
567            destinations_vec
568                .try_reserve_exact(values.len())
569                .map_err(|_| {
570                    self.budget
571                        .scoped_allocation_failed(scratch_bytes, operation)
572                })?;
573            order_vec.extend(0..values.len());
574            destinations_vec.resize(values.len(), 0usize);
575            Some(reservation)
576        } else {
577            None
578        };
579        let mut inline_order = [0usize; super::sort::INLINE_SORT_LIMIT];
580        let mut inline_destinations = [0usize; super::sort::INLINE_SORT_LIMIT];
581        let (mut order, mut destinations) = if values.len() > super::sort::INLINE_SORT_LIMIT {
582            (order_vec.as_mut_slice(), destinations_vec.as_mut_slice())
583        } else {
584            for (index, slot) in inline_order[..values.len()].iter_mut().enumerate() {
585                *slot = index;
586            }
587            (
588                &mut inline_order[..values.len()],
589                &mut inline_destinations[..values.len()],
590            )
591        };
592        // Merge index runs fallibly. No record moves before all work is admitted.
593        let mut width = 1usize;
594        while width < values.len() {
595            // Each output index is read and written; comparisons read two more.
596            self.charge_work(
597                scratch_bytes
598                    .checked_mul(2)
599                    .ok_or_else(|| self.refuse_codec_limit(operation, u64::MAX - 1, u64::MAX))?,
600                operation,
601            )?;
602            let mut start = 0;
603            while start < values.len() {
604                let middle = start + width.min(values.len() - start);
605                let end = middle + width.min(values.len() - middle);
606                let (mut left, mut right) = (start, middle);
607                for destination in &mut destinations[start..end] {
608                    if left < middle
609                        && (right == end || !compared(order[left], order[right])?.is_gt())
610                    {
611                        *destination = order[left];
612                        left += 1;
613                    } else {
614                        *destination = order[right];
615                        right += 1;
616                    }
617                }
618                start = end;
619            }
620            std::mem::swap(&mut order, &mut destinations);
621            width += width.min(values.len() - width);
622        }
623        // The inverse permutation and at most N-1 swaps have linear move work.
624        let permutation = super::u64_from_index(std::mem::size_of::<T>())
625            .checked_mul(4)
626            .and_then(|bytes| bytes.checked_add(index_bytes.checked_mul(8)?))
627            .and_then(|bytes| count.checked_mul(bytes))
628            .ok_or_else(|| self.refuse_codec_limit(operation, u64::MAX - 1, u64::MAX))?;
629        self.charge_work(permutation, operation)?;
630        for (destination, &source) in order.iter().enumerate() {
631            destinations[source] = destination;
632        }
633        for index in 0..values.len() {
634            while destinations[index] != index {
635                let destination = destinations[index];
636                values.swap(index, destination);
637                destinations.swap(index, destination);
638            }
639        }
640        Ok(())
641    }
642
643    /// Returns the resource refusal that has fused this decode session.
644    pub fn resource_refusal(&self) -> Option<ResourceLimit> {
645        self.budget.fused()
646    }
647
648    /// Permanently refuses a codec-local resource request.
649    ///
650    /// Codecs use this when a bounded recovery algorithm reaches a fixed
651    /// local ceiling instead of a session-wide dimension. The refusal fuses
652    /// the session so a caller cannot accidentally turn it into a semantic
653    /// fallback or report success after the limit was reached.
654    pub(crate) fn refuse_local_limit(
655        &self,
656        operation: &'static str,
657        limit: u64,
658        requested: u64,
659    ) -> ResourceLimit {
660        let (used, additional) = match requested.checked_sub(limit) {
661            Some(excess) => (limit, excess),
662            None => (requested, 0),
663        };
664        self.budget.refuse_limit(
665            ResourceDimension::Codec(operation),
666            ResourceFailure::BudgetExceeded,
667            limit,
668            used,
669            additional,
670            operation,
671        )
672    }
673
674    /// Permanently refuses a codec-local resource request.
675    pub fn refuse_codec_limit(
676        &self,
677        operation: &'static str,
678        limit: u64,
679        requested: u64,
680    ) -> CodecError {
681        self.refuse_local_limit(operation, limit, requested).into()
682    }
683
684    /// Creates a local work slice that also draws from the session allowance.
685    pub fn work_budget(&self, local_limit: u64) -> WorkBudget<'_> {
686        WorkBudget::for_session(local_limit, &self.budget)
687    }
688
689    // --- decompression ------------------------------------------------------
690
691    /// Begins an expansion whose output is charged incrementally and becomes
692    /// available only after successful finalization.
693    pub fn begin_expand(&self, spec: ExpandSpec) -> Result<ExpandWriter<'_, 'a>, CodecError> {
694        if let Some(limit) = self.budget.fused() {
695            return Err(CodecError::ResourceLimit(limit));
696        }
697        if let ExpandSpec::Exact(size) = spec {
698            let per_expand = self.per_expand_allowance();
699            if size > per_expand {
700                return Err(self.fuse(
701                    ResourceFailure::BudgetExceeded,
702                    LimitScope::PerExpand,
703                    size,
704                    "begin_expand",
705                ));
706            }
707            if self
708                .decompression_allowance()
709                .checked_sub(self.budget.decompressed_used())
710                .is_none_or(|remaining| size > remaining)
711            {
712                return Err(self.fuse(
713                    ResourceFailure::BudgetExceeded,
714                    LimitScope::Global,
715                    size,
716                    "begin_expand",
717                ));
718            }
719        }
720        let mut buffer: Vec<u8> = Vec::new();
721        let reserve = match spec {
722            // A declared size the address space cannot name is above the cap,
723            // so the cap is the reservation either way.
724            ExpandSpec::Exact(size) => match usize::try_from(size) {
725                Ok(size) => size.min(RESERVE_CLAMP),
726                Err(_) => RESERVE_CLAMP,
727            },
728            ExpandSpec::Unknown => 0,
729        };
730        if reserve > 0 {
731            buffer.try_reserve(reserve).map_err(|_| {
732                self.fuse(
733                    ResourceFailure::AllocationFailed,
734                    LimitScope::PerExpand,
735                    u64_from_index(reserve),
736                    "begin_expand",
737                )
738            })?;
739        }
740        Ok(ExpandWriter {
741            ctx: self,
742            spec,
743            buffer,
744        })
745    }
746
747    /// Copies several input extents into one derived view.
748    pub fn concat_views(&self, inputs: &[View<'_>]) -> Result<View<'a>, CodecError> {
749        if let Some(limit) = self.budget.fused() {
750            return Err(CodecError::ResourceLimit(limit));
751        }
752        if inputs.is_empty() {
753            return Err(CodecError::Malformed(
754                "cannot concatenate an empty view list".into(),
755            ));
756        }
757        self.charge_work(u64_from_index(inputs.len()), "concat_views")?;
758        let total = inputs.iter().try_fold(0usize, |total, view| {
759            total.checked_add(view.window().len()).ok_or_else(|| {
760                self.budget.refuse(
761                    ResourceDimension::RetainedBytes,
762                    ResourceFailure::BudgetExceeded,
763                    self.budget.policy().limits.max_retained_bytes,
764                    u64_from_index(total),
765                    u64_from_index(view.window().len()),
766                    "concat_views",
767                )
768            })
769        })?;
770        self.charge_work(u64_from_index(total), "concat_views")?;
771        let reservation = self.reserve_scoped(u64_from_index(total), "concat_views")?;
772        let mut buffer = Vec::new();
773        buffer.try_reserve_exact(total).map_err(|_| {
774            self.budget.refuse(
775                ResourceDimension::MaterializedBytes,
776                ResourceFailure::AllocationFailed,
777                self.budget.policy().limits.max_materialized_bytes,
778                0,
779                u64_from_index(total),
780                "concat_views",
781            )
782        })?;
783        for view in inputs {
784            buffer.extend_from_slice(view.window());
785        }
786        let bytes = self.arena.alloc(self, buffer.into_boxed_slice())?;
787        reservation.commit()?;
788        let space = self.allocate_space()?;
789        Ok(View::over_space(bytes, space))
790    }
791
792    /// Concatenates owned chunks into one retained buffer without an arena copy.
793    pub fn concat_retained(
794        &self,
795        inputs: &[Vec<u8>],
796        operation: &'static str,
797    ) -> Result<Vec<u8>, CodecError> {
798        if let Some(limit) = self.budget.fused() {
799            return Err(CodecError::ResourceLimit(limit));
800        }
801        if inputs.is_empty() {
802            return Err(CodecError::Malformed(
803                "cannot concatenate an empty buffer list".into(),
804            ));
805        }
806        let total = inputs.iter().try_fold(0_usize, |total, input| {
807            total.checked_add(input.len()).ok_or_else(|| {
808                CodecError::NotImplemented("retained concatenation exceeds usize".into())
809            })
810        })?;
811        let total_bytes = crate::decode::u64_from_index(total);
812        self.charge_work(u64_from_index(inputs.len()), operation)?;
813        self.charge_work(total_bytes, operation)?;
814        self.charge_retained(total_bytes, operation)?;
815        let mut buffer = Vec::new();
816        buffer.try_reserve_exact(total).map_err(|_| {
817            self.budget
818                .retained_allocation_failed(total_bytes, operation)
819        })?;
820        for input in inputs {
821            buffer.extend_from_slice(input);
822        }
823        Ok(buffer)
824    }
825
826    /// Registers a stored (uncompressed) child range as a space that borrows
827    /// the parent bytes without copying.
828    ///
829    /// `range` is expressed in the parent view's own space coordinates and must
830    /// lie within the parent window; a range that escapes the parent is refused
831    /// here, at the request site, exactly as [`View::child`] refuses. No bytes
832    /// are copied. It is the archive-entry counterpart of [`DecodeContext::begin_expand`] —
833    /// stored ZIP entries take this path, compressed ones take the expander.
834    /// Registration still refuses on a fused context so a stored entry cannot be
835    /// admitted after a refusal.
836    pub fn register_slice<'v>(
837        &self,
838        parent: View<'v>,
839        range: ByteRange,
840    ) -> Result<View<'v>, CodecError> {
841        if let Some(limit) = self.budget.fused() {
842            return Err(CodecError::ResourceLimit(limit));
843        }
844        let start = usize::try_from(range.start).ok();
845        let end = usize::try_from(range.end).ok();
846        let child = start
847            .zip(end)
848            .and_then(|(start, end)| parent.child(start, end))
849            .ok_or_else(|| {
850                CodecError::malformed(format_args!(
851                    "stored slice [{}, {}) escapes parent space {}",
852                    range.start,
853                    range.end,
854                    parent.space().index()
855                ))
856            })?;
857        self.charge_collection_items(1, "register borrowed space")?;
858        let space = self.allocate_space()?;
859        Ok(View::over_space(child.window(), space))
860    }
861
862    // --- lifecycle ----------------------------------------------------------
863
864    /// Closes a decode or inspection session, returning a fused resource error
865    /// even when codec code swallowed the charge that caused it.
866    pub fn finish_session(self) -> Result<(), CodecError> {
867        if let Some(limit) = self.budget.fused() {
868            return Err(CodecError::ResourceLimit(limit));
869        }
870        Ok(())
871    }
872}
873
874fn root_limit(reason: ResourceFailure, limit: u64, used: u64) -> ResourceLimit {
875    if used <= limit {
876        return ResourceLimit {
877            dimension: ResourceDimension::InputBytes,
878            reason,
879            limit,
880            used,
881            additional: 0,
882            operation: "read_root",
883        };
884    }
885    let additional = used - limit;
886    ResourceLimit {
887        dimension: ResourceDimension::InputBytes,
888        reason,
889        limit,
890        used,
891        additional,
892        operation: "read_root",
893    }
894}
895
896/// How much output an expansion is expected to produce.
897#[derive(Debug, Clone, Copy, PartialEq, Eq)]
898pub enum ExpandSpec {
899    /// A declared exact size, enforced per-write and at finalize.
900    Exact(u64),
901    /// No trustworthy declared size: the decompression limits apply.
902    Unknown,
903}
904
905/// Writes decompressed output under incremental charging.
906#[derive(Debug)]
907pub struct ExpandWriter<'ctx, 'a> {
908    ctx: &'ctx DecodeContext<'a>,
909    spec: ExpandSpec,
910    buffer: Vec<u8>,
911}
912
913impl<'a> ExpandWriter<'_, 'a> {
914    /// Appends decompressed output, charging before it is retained.
915    pub fn write(&mut self, data: &[u8]) -> Result<(), CodecError> {
916        let len = u64_from_index(data.len());
917        let new_written = self.written().checked_add(len).ok_or_else(|| {
918            self.ctx.fuse(
919                ResourceFailure::BudgetExceeded,
920                LimitScope::PerExpand,
921                len,
922                "expand_write",
923            )
924        })?;
925        match self.spec {
926            ExpandSpec::Exact(size) if new_written > size => {
927                return Err(CodecError::malformed(format_args!(
928                    "expansion exceeded declared exact size {size}"
929                )))
930            }
931            _ => {}
932        }
933        let per_expand = self.ctx.per_expand_allowance();
934        if new_written > per_expand {
935            return Err(self.ctx.fuse(
936                ResourceFailure::BudgetExceeded,
937                LimitScope::PerExpand,
938                len,
939                "expand_write",
940            ));
941        }
942        self.ctx.budget.charge_decompressed(len, "expand_write")?;
943        self.buffer.try_reserve(data.len()).map_err(|_| {
944            self.ctx.fuse(
945                ResourceFailure::AllocationFailed,
946                LimitScope::PerExpand,
947                len,
948                "expand_write",
949            )
950        })?;
951        self.buffer.extend_from_slice(data);
952        Ok(())
953    }
954
955    /// Finalizes the expansion, stores it in the arena, and registers its space.
956    pub fn finalize(self) -> Result<View<'a>, CodecError> {
957        self.check_exact()?;
958        let bytes = self
959            .ctx
960            .arena
961            .alloc(self.ctx, self.buffer.into_boxed_slice())?;
962        let space = self.ctx.allocate_space()?;
963        Ok(View::over_space(bytes, space))
964    }
965
966    /// Finalizes an expansion directly into a caller-owned buffer.
967    /// The expansion budget already admits its bytes, so no retained copy occurs.
968    pub fn finalize_owned(self) -> Result<Vec<u8>, CodecError> {
969        self.check_exact()?;
970        Ok(self.buffer)
971    }
972
973    fn check_exact(&self) -> Result<(), CodecError> {
974        if let ExpandSpec::Exact(size) = self.spec {
975            if self.written() != size {
976                return Err(CodecError::malformed(format_args!(
977                    "expansion produced {} of declared exact {size} bytes",
978                    self.written()
979                )));
980            }
981        }
982        Ok(())
983    }
984
985    /// Returns how many bytes have been written so far.
986    pub fn written(&self) -> u64 {
987        u64_from_index(self.buffer.len())
988    }
989}
990
991#[cfg(test)]
992mod tests {
993    use super::{u64_from_index, ByteRange, DecodeArena, DecodeContext, DecodePolicy};
994    use crate::decode::{ResourceDimension, ResourceFailure};
995    use std::io::{self, Cursor, Read, Seek, SeekFrom};
996
997    #[test]
998    fn temporary_byte_storage_uses_its_byte_budget_and_releases_it() {
999        let arena = DecodeArena::new();
1000        let mut policy = DecodePolicy::service();
1001        policy.limits.max_materialized_bytes = 32;
1002        policy.limits.max_collection_items = 0;
1003        let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("context");
1004        let (reservation, bytes) = ctx
1005            .try_materialized(32, "test bytes", || {
1006                let mut bytes = Vec::<u8>::new();
1007                bytes.try_reserve_exact(32)?;
1008                Ok(bytes)
1009            })
1010            .expect("byte allowance");
1011        assert_eq!(bytes.capacity(), 32);
1012        drop(reservation);
1013        let (reservation, ()) = ctx
1014            .try_materialized(32, "test bytes", || Ok(()))
1015            .expect("released allowance");
1016        assert!(
1017            matches!(ctx.try_materialized(1, "test bytes", || Ok(())), Err(crate::CodecError::ResourceLimit(limit)) if limit.dimension == ResourceDimension::MaterializedBytes)
1018        );
1019        drop(reservation);
1020    }
1021
1022    #[test]
1023    fn loaded_input_processing_preserves_proportional_and_absolute_limits() {
1024        let input_bytes = 1024;
1025        let allowance = crate::decode::policy::MATERIALIZED_BASE
1026            + input_bytes * crate::decode::policy::MATERIALIZED_PER_INPUT_BYTE;
1027        let arena = DecodeArena::new();
1028        let mut policy = DecodePolicy::service();
1029        let ctx = DecodeContext::for_loaded_input(&arena, &policy, input_bytes)
1030            .expect("admitted input length");
1031        drop(
1032            ctx.reserve_scoped(allowance, "loaded input allowance")
1033                .expect("input-proportional allowance"),
1034        );
1035        assert!(
1036            matches!(ctx.reserve_scoped(allowance + 1, "loaded input allowance"),
1037            Err(crate::CodecError::ResourceLimit(limit))
1038            if limit.dimension == ResourceDimension::MaterializedBytes && limit.limit == allowance)
1039        );
1040
1041        policy.limits.max_materialized_bytes = 32;
1042        let ctx = DecodeContext::for_loaded_input(&arena, &policy, input_bytes)
1043            .expect("admitted input length");
1044        assert!(matches!(ctx.reserve_scoped(33, "absolute limit"),
1045            Err(crate::CodecError::ResourceLimit(limit))
1046            if limit.dimension == ResourceDimension::MaterializedBytes && limit.limit == 32));
1047
1048        policy.limits.max_input_bytes = input_bytes - 1;
1049        assert!(
1050            matches!(DecodeContext::for_loaded_input(&arena, &policy, input_bytes),
1051            Err(crate::CodecError::ResourceLimit(limit))
1052            if limit.dimension == ResourceDimension::InputBytes && limit.additional == input_bytes)
1053        );
1054    }
1055
1056    #[test]
1057    fn zero_collection_limit_refuses_second_empty_finalization() {
1058        let arena = DecodeArena::new();
1059        let mut policy = DecodePolicy::service();
1060        policy.limits.max_collection_items = 0;
1061        let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("empty root");
1062        for _ in 0..2 {
1063            let result = ctx
1064                .begin_expand(super::ExpandSpec::Exact(0))
1065                .and_then(super::ExpandWriter::finalize);
1066            assert!(
1067                matches!(result, Err(crate::CodecError::ResourceLimit(limit))
1068                if limit.dimension == ResourceDimension::CollectionItems)
1069            );
1070        }
1071    }
1072
1073    #[test]
1074    fn one_collection_slot_admits_only_one_empty_finalization() {
1075        let arena = DecodeArena::new();
1076        let mut policy = DecodePolicy::service();
1077        policy.limits.max_collection_items = 1;
1078        let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("empty root");
1079        assert!(ctx
1080            .begin_expand(super::ExpandSpec::Exact(0))
1081            .expect("empty expansion")
1082            .finalize()
1083            .expect("first registry slot")
1084            .window()
1085            .is_empty());
1086        assert!(
1087            matches!(ctx.begin_expand(super::ExpandSpec::Exact(0)).expect("empty expansion")
1088            .finalize(), Err(crate::CodecError::ResourceLimit(limit))
1089            if limit.dimension == ResourceDimension::CollectionItems && limit.used == 1)
1090        );
1091    }
1092
1093    struct RewindFails(Cursor<Vec<u8>>);
1094
1095    impl Read for RewindFails {
1096        fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
1097            self.0.read(buffer)
1098        }
1099    }
1100
1101    impl Seek for RewindFails {
1102        fn seek(&mut self, position: SeekFrom) -> io::Result<u64> {
1103            if position == SeekFrom::Start(0) {
1104                Err(io::Error::new(
1105                    io::ErrorKind::PermissionDenied,
1106                    "rewind denied",
1107                ))
1108            } else {
1109                self.0.seek(position)
1110            }
1111        }
1112    }
1113
1114    #[test]
1115    fn retained_collection_refuses_slot_before_copying_the_record() {
1116        let arena = DecodeArena::new();
1117        let mut policy = DecodePolicy::service();
1118        policy.limits.max_retained_bytes = 0;
1119        let (ctx, _) =
1120            DecodeContext::from_root_bytes(&[], &arena, &policy).expect("admitted test operation");
1121        let copied = std::cell::Cell::new(false);
1122        let result = ctx.try_collect_retained_with([1_u8], "collect record", |value| {
1123            copied.set(true);
1124            Ok::<_, crate::CodecError>(value)
1125        });
1126        assert!(
1127            matches!(result, Err(crate::CodecError::ResourceLimit(limit))
1128            if limit.dimension == super::ResourceDimension::RetainedBytes && limit.additional == 1)
1129        );
1130        assert!(!copied.get());
1131    }
1132
1133    #[test]
1134    fn scoped_copy_storage_is_released_and_does_not_charge_retained_bytes() {
1135        let arena = DecodeArena::new();
1136        let mut policy = DecodePolicy::service();
1137        policy.limits.max_retained_bytes = 0;
1138        policy.limits.max_materialized_bytes = 4;
1139        let (ctx, _) =
1140            DecodeContext::from_root_bytes(&[], &arena, &policy).expect("admitted test operation");
1141        let (text, reservation) = ctx
1142            .with_scoped_storage("temporary copy", || {
1143                ctx.copy_retained_text("abcd", "temporary copy")
1144            })
1145            .expect("admitted test operation");
1146        assert_eq!(text, "abcd");
1147        drop(text);
1148        drop(reservation);
1149        assert!(ctx.reserve_scoped(4, "reuse scope").is_ok());
1150    }
1151
1152    #[test]
1153    fn scoped_copy_storage_refuses_materialized_bytes() {
1154        let arena = DecodeArena::new();
1155        let mut policy = DecodePolicy::service();
1156        policy.limits.max_materialized_bytes = 3;
1157        let (ctx, _) =
1158            DecodeContext::from_root_bytes(&[], &arena, &policy).expect("admitted test operation");
1159        assert!(matches!(ctx.with_scoped_storage("temporary copy", || {
1160            ctx.copy_retained_text("abcd", "temporary copy")
1161        }), Err(crate::CodecError::ResourceLimit(limit))
1162            if limit.dimension == super::ResourceDimension::MaterializedBytes
1163                && limit.additional == 4));
1164    }
1165
1166    #[test]
1167    fn scoped_copy_extends_existing_reservation_without_double_charging() {
1168        let arena = DecodeArena::new();
1169        let mut policy = DecodePolicy::service();
1170        policy.limits.max_materialized_bytes = 6;
1171        policy.limits.max_retained_bytes = 0;
1172        let (ctx, _) =
1173            DecodeContext::from_root_bytes(&[], &arena, &policy).expect("admitted test operation");
1174        let mut storage = ctx
1175            .reserve_scoped(2, "temporary record")
1176            .expect("admitted test operation");
1177        let text = storage
1178            .with_storage(|| ctx.copy_retained_text("abcd", "temporary text"))
1179            .expect("admitted test operation");
1180        assert_eq!(text, "abcd");
1181        assert!(
1182            matches!(ctx.reserve_scoped(1, "full scope"), Err(crate::CodecError::ResourceLimit(limit)) if limit.used == 6)
1183        );
1184        drop(text);
1185        drop(storage);
1186    }
1187
1188    #[test]
1189    fn nested_scoped_copies_hold_independent_reservations() {
1190        let arena = DecodeArena::new();
1191        let mut policy = DecodePolicy::service();
1192        policy.limits.max_materialized_bytes = 6;
1193        policy.limits.max_retained_bytes = 0;
1194        let (ctx, _) =
1195            DecodeContext::from_root_bytes(&[], &arena, &policy).expect("admitted test operation");
1196        let ((first, second, inner), outer) = ctx
1197            .with_scoped_storage("outer", || {
1198                let first = ctx.copy_retained_text("ab", "outer")?;
1199                let (second, inner) =
1200                    ctx.with_scoped_storage("inner", || ctx.copy_retained_text("cd", "inner"))?;
1201                Ok::<_, crate::CodecError>((first, second, inner))
1202            })
1203            .expect("admitted test operation");
1204        assert_eq!((first.as_str(), second.as_str()), ("ab", "cd"));
1205        assert!(ctx.reserve_scoped(2, "remaining scope").is_ok());
1206        drop(first);
1207        drop(outer);
1208        assert!(ctx.reserve_scoped(4, "outer released").is_ok());
1209        drop(second);
1210        drop(inner);
1211        assert!(ctx.reserve_scoped(6, "all released").is_ok());
1212    }
1213
1214    #[test]
1215    fn root_reader_preserves_bytes_across_fixed_chunk_boundaries() {
1216        let mut bytes = vec![0x5a_u8; 8193];
1217        bytes[8192] = 0x7f;
1218        let mut reader = Cursor::new(bytes.clone());
1219        let arena = DecodeArena::new();
1220        let (_, root) =
1221            DecodeContext::read_root(&mut reader, &arena, &DecodePolicy::default(), false)
1222                .expect("root is admitted");
1223        assert_eq!(root.window(), bytes.as_slice());
1224    }
1225
1226    #[test]
1227    fn root_reader_propagates_failed_rewind_after_a_successful_size_probe() {
1228        let arena = DecodeArena::new();
1229        let mut reader = RewindFails(Cursor::new(b"not empty".to_vec()));
1230        assert!(matches!(
1231            DecodeContext::read_root(&mut reader, &arena, &DecodePolicy::default(), false),
1232            Err(crate::CodecError::Io(error)) if error.kind() == io::ErrorKind::PermissionDenied
1233        ));
1234    }
1235
1236    #[test]
1237    fn exhausted_space_ids_refuse_registration_without_reusing_an_id() {
1238        let arena = DecodeArena::new();
1239        let (ctx, root) = DecodeContext::from_root_bytes(b"x", &arena, &DecodePolicy::default())
1240            .expect("test input fits the policy");
1241        ctx.derived_spaces.set(usize::MAX - 1);
1242        let range = ByteRange { start: 0, end: 1 };
1243        let view = ctx
1244            .register_slice(root, range)
1245            .expect("maximum space id is allocatable");
1246        assert_eq!(view.space().index(), usize::MAX);
1247        let error = ctx
1248            .register_slice(root, range)
1249            .expect_err("space IDs are exhausted");
1250        let crate::CodecError::ResourceLimit(limit) = error else {
1251            panic!("exhausted IDs return a resource refusal");
1252        };
1253        assert_eq!(limit.limit, u64_from_index(usize::MAX));
1254        assert_eq!(limit.used, u64_from_index(usize::MAX));
1255        assert_eq!(limit.additional, 1);
1256        assert!(ctx.register_slice(root, range).is_err());
1257        assert!(ctx.finish_session().is_err());
1258    }
1259
1260    #[test]
1261    fn collection_reservation_reports_allocator_refusal_after_charge() {
1262        let arena = DecodeArena::new();
1263        let mut policy = DecodePolicy::service();
1264        policy.limits.max_collection_items = u64::MAX;
1265        policy.limits.max_retained_bytes = u64::MAX;
1266        let (ctx, _) = DecodeContext::from_root_bytes(b"x", &arena, &policy)
1267            .expect("test input fits the policy");
1268        let mut values = Vec::<u8>::new();
1269        let error = ctx
1270            .reserve_vec(&mut values, usize::MAX, "test collection reservation")
1271            .expect_err("a vector cannot reserve more than isize::MAX bytes");
1272        assert!(matches!(
1273            error,
1274            crate::CodecError::ResourceLimit(limit)
1275                if limit.dimension == ResourceDimension::RetainedBytes
1276                    && limit.reason == ResourceFailure::AllocationFailed
1277                    && limit.used == 0
1278                    && limit.additional == u64_from_index(usize::MAX)
1279                    && limit.operation == "test collection reservation"
1280        ));
1281        assert!(ctx.finish_session().is_err());
1282    }
1283
1284    #[test]
1285    fn try_collection_refuses_before_allocation_and_succeeds_under_service_profile() {
1286        let arena = DecodeArena::new();
1287        let mut policy = DecodePolicy::service();
1288        policy.limits.max_collection_items = 2;
1289        let (ctx, _) = DecodeContext::from_root_bytes(b"x", &arena, &policy)
1290            .expect("test input fits the policy");
1291        let mut allocated = false;
1292        let error = ctx
1293            .try_collection(3, "test collection allocation", || {
1294                allocated = true;
1295                Ok(())
1296            })
1297            .expect_err("three items exceed a limit of two");
1298        assert!(!allocated);
1299        assert!(matches!(
1300            error,
1301            crate::CodecError::ResourceLimit(limit)
1302                if limit.dimension == ResourceDimension::CollectionItems
1303                    && limit.reason == ResourceFailure::BudgetExceeded
1304                    && limit.operation == "test collection allocation"
1305        ));
1306
1307        let arena = DecodeArena::new();
1308        let (ctx, _) = DecodeContext::from_root_bytes(b"x", &arena, &DecodePolicy::service())
1309            .expect("test input fits the policy");
1310        let mut values = Vec::<u8>::new();
1311        ctx.try_collection(3, "test collection allocation", || values.try_reserve(3))
1312            .expect("the service profile admits three items");
1313        assert!(values.capacity() >= 3);
1314    }
1315
1316    #[test]
1317    fn try_collection_reports_allocator_refusal_after_charge() {
1318        let arena = DecodeArena::new();
1319        let mut policy = DecodePolicy::service();
1320        policy.limits.max_collection_items = u64::MAX;
1321        let (ctx, _) = DecodeContext::from_root_bytes(b"x", &arena, &policy)
1322            .expect("test input fits the policy");
1323        let mut values = Vec::<u8>::new();
1324        let error = ctx
1325            .try_collection(usize::MAX, "test collection allocation", || {
1326                values.try_reserve(usize::MAX)
1327            })
1328            .expect_err("a vector cannot reserve more than isize::MAX bytes");
1329        assert!(matches!(
1330            error,
1331            crate::CodecError::ResourceLimit(limit)
1332                if limit.dimension == ResourceDimension::CollectionItems
1333                    && limit.reason == ResourceFailure::AllocationFailed
1334                    && limit.operation == "test collection allocation"
1335        ));
1336    }
1337
1338    #[test]
1339    fn scoped_text_copy_refuses_before_allocation_and_releases_on_drop() {
1340        let arena = DecodeArena::new();
1341        let mut policy = DecodePolicy::service();
1342        policy.limits.max_materialized_bytes = 3;
1343        let (ctx, _) = DecodeContext::from_root_bytes(b"x", &arena, &policy)
1344            .expect("test input fits the policy");
1345        {
1346            let mut reservation = ctx
1347                .reserve_scoped(0, "test scoped text")
1348                .expect("empty reservation is admitted");
1349            let copy = ctx
1350                .copy_scoped_text("abc", &mut reservation, "test scoped text")
1351                .expect("three temporary bytes are admitted");
1352            assert_eq!(copy, "abc");
1353        }
1354        {
1355            let mut reservation = ctx
1356                .reserve_scoped(0, "test scoped text")
1357                .expect("empty reservation is admitted");
1358            assert_eq!(
1359                ctx.copy_scoped_text("def", &mut reservation, "test scoped text")
1360                    .expect("the prior reservation was released"),
1361                "def"
1362            );
1363        }
1364        let mut reservation = ctx
1365            .reserve_scoped(0, "test scoped text")
1366            .expect("empty reservation is admitted");
1367        let error = ctx
1368            .copy_scoped_text("abcd", &mut reservation, "test scoped text")
1369            .expect_err("four bytes exceed the temporary limit");
1370        assert!(matches!(error, crate::CodecError::ResourceLimit(limit)
1371            if limit.dimension == ResourceDimension::MaterializedBytes
1372                && limit.operation == "test scoped text"));
1373    }
1374
1375    #[test]
1376    fn scoped_format_refuses_before_allocation_and_releases_on_drop() {
1377        let arena = DecodeArena::new();
1378        let mut policy = DecodePolicy::service();
1379        policy.limits.max_materialized_bytes = 3;
1380        let (ctx, _) =
1381            DecodeContext::from_root_bytes(&[], &arena, &policy).expect("empty root is admitted");
1382        {
1383            let (text, _reservation) = ctx
1384                .format_scoped(format_args!("x{}", 12), "test scoped format")
1385                .expect("three rendered bytes fit");
1386            assert_eq!(text, "x12");
1387        }
1388        assert_eq!(
1389            ctx.format_scoped(format_args!("y{}", 34), "test scoped format")
1390                .expect("prior reservation was released")
1391                .0,
1392            "y34"
1393        );
1394        let error = ctx
1395            .format_scoped(format_args!("z{}", 345), "test scoped format")
1396            .expect_err("four bytes exceed the materialized allowance");
1397        assert!(matches!(error, crate::CodecError::ResourceLimit(limit)
1398            if limit.dimension == ResourceDimension::MaterializedBytes
1399                && limit.operation == "test scoped format"));
1400    }
1401
1402    #[test]
1403    fn retained_format_refuses_before_string_growth() {
1404        let arena = DecodeArena::new();
1405        let mut policy = DecodePolicy::service();
1406        policy.limits.max_retained_bytes = 4;
1407        let (ctx, _) =
1408            DecodeContext::from_root_bytes(&[], &arena, &policy).expect("empty root is admitted");
1409        assert_eq!(
1410            ctx.format_retained(
1411                format_args!("{number}", number = 1234),
1412                "test retained format"
1413            )
1414            .expect("four rendered bytes fit"),
1415            "1234"
1416        );
1417        let error = ctx
1418            .format_retained(format_args!("{}", 5), "test retained format")
1419            .expect_err("one more byte exceeds the retained limit");
1420        assert!(matches!(error, crate::CodecError::ResourceLimit(limit)
1421            if limit.dimension == ResourceDimension::RetainedBytes
1422                && limit.operation == "test retained format"));
1423    }
1424    #[test]
1425    fn string_growth_charges_only_new_capacity() {
1426        let arena = DecodeArena::new();
1427        let mut policy = DecodePolicy::service();
1428        policy.limits.max_retained_bytes = 0;
1429        let (ctx, _) =
1430            DecodeContext::from_root_bytes(&[], &arena, &policy).expect("admitted test operation");
1431        let mut text = String::with_capacity(4);
1432        ctx.try_reserve_retained_text(&mut text, 4, "existing text capacity")
1433            .expect("admitted test operation");
1434        assert_eq!(text.capacity(), 4);
1435        let error = ctx
1436            .try_reserve_retained_text(&mut text, 5, "new text capacity")
1437            .expect_err("test operation refuses");
1438        assert!(matches!(error, crate::CodecError::ResourceLimit(limit)
1439            if limit.dimension == ResourceDimension::RetainedBytes
1440                && limit.used == 0 && limit.additional == 1));
1441        assert_eq!(text.capacity(), 4);
1442    }
1443
1444    #[test]
1445    fn text_append_reserves_exact_growth() {
1446        let arena = DecodeArena::new();
1447        let mut policy = DecodePolicy::service();
1448        policy.limits.max_retained_bytes = 3;
1449        let (ctx, _) =
1450            DecodeContext::from_root_bytes(&[], &arena, &policy).expect("admitted test operation");
1451        let mut text = ctx
1452            .copy_retained_text("ab", "initial text")
1453            .expect("admitted test operation");
1454        ctx.append_retained(&mut text, "c", "append text")
1455            .expect("admitted test operation");
1456        assert_eq!(text, "abc");
1457        assert_eq!(text.capacity(), 3);
1458        let error = ctx
1459            .append_retained(&mut text, "d", "append text")
1460            .expect_err("test operation refuses");
1461        assert!(matches!(error, crate::CodecError::ResourceLimit(limit)
1462            if limit.dimension == ResourceDimension::RetainedBytes
1463                && limit.used == 3 && limit.additional == 1));
1464        assert_eq!(text, "abc");
1465    }
1466}
1467
1468#[cfg(test)]
1469mod sort_tests;