Skip to main content

cadmpeg_core/decode/
policy.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Decode modes, resource ceilings, and input-proportional allowances.
3
4#[cfg(feature = "schema")]
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8const MIB: u64 = 1024 * 1024;
9const GIB: u64 = 1024 * MIB;
10
11/// Whether a successful decode may report mandatory transfer losses.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
13#[cfg_attr(feature = "schema", derive(JsonSchema))]
14#[serde(rename_all = "snake_case")]
15#[serde(deny_unknown_fields)]
16pub enum DecodeMode {
17    /// Reject a completed decode that reports a mandatory transfer loss.
18    Strict,
19    /// Return the decoded result with its loss report.
20    #[default]
21    Salvage,
22}
23
24/// Absolute resource ceilings.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[cfg_attr(feature = "schema", derive(JsonSchema))]
27#[serde(deny_unknown_fields)]
28pub struct ResourceLimits {
29    /// Maximum physical input bytes read at the root.
30    pub max_input_bytes: u64,
31    /// Maximum cumulative decompressed bytes across all expansions.
32    pub max_decompressed_bytes_total: u64,
33    /// Maximum decompressed bytes produced by any single expansion.
34    pub max_decompressed_bytes_per_expand: u64,
35    /// Maximum bytes held by temporary materializations at one time.
36    #[serde(default = "default_max_materialized_bytes")]
37    pub max_materialized_bytes: u64,
38    /// Maximum cumulative bytes retained outside expansion output.
39    #[serde(default = "default_max_retained_bytes")]
40    pub max_retained_bytes: u64,
41    /// Maximum admitted semantic or native entities.
42    #[serde(default = "default_max_entities")]
43    pub max_entities: u64,
44    /// Maximum admitted items across decoded collections.
45    #[serde(default = "default_max_collection_items")]
46    pub max_collection_items: u64,
47    /// Maximum active recursive nesting depth.
48    #[serde(default = "default_max_recursion_depth")]
49    pub max_recursion_depth: u64,
50    /// Maximum cumulative algorithm work units.
51    #[serde(default = "default_max_work_units")]
52    pub max_work_units: u64,
53}
54
55impl ResourceLimits {
56    /// Generous ceilings for interactive desktop use; the default profile.
57    pub const fn desktop() -> Self {
58        Self {
59            max_input_bytes: 4 * GIB,
60            max_decompressed_bytes_total: 8 * GIB,
61            max_decompressed_bytes_per_expand: 2 * GIB,
62            max_materialized_bytes: 4 * GIB,
63            max_retained_bytes: 4 * GIB,
64            max_entities: 64_000_000,
65            max_collection_items: 16_000_000,
66            max_recursion_depth: 256,
67            max_work_units: 4_000_000_000,
68        }
69    }
70
71    /// Tight ceilings for unattended service use.
72    pub const fn service() -> Self {
73        Self {
74            max_input_bytes: 256 * MIB,
75            max_decompressed_bytes_total: GIB,
76            max_decompressed_bytes_per_expand: 256 * MIB,
77            max_materialized_bytes: 512 * MIB,
78            max_retained_bytes: 512 * MIB,
79            max_entities: 8_000_000,
80            max_collection_items: 1_000_000,
81            max_recursion_depth: 128,
82            max_work_units: 256_000_000,
83        }
84    }
85}
86
87impl Default for ResourceLimits {
88    fn default() -> Self {
89        Self::desktop()
90    }
91}
92
93/// Decode mode and resource ceilings.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
95#[cfg_attr(feature = "schema", derive(JsonSchema))]
96#[serde(deny_unknown_fields)]
97pub struct DecodePolicy {
98    /// How mandatory transfer losses are handled.
99    pub mode: DecodeMode,
100    /// The absolute ceilings.
101    pub limits: ResourceLimits,
102}
103
104impl DecodePolicy {
105    /// The default desktop profile in salvage mode.
106    pub const fn desktop() -> Self {
107        Self {
108            mode: DecodeMode::Salvage,
109            limits: ResourceLimits::desktop(),
110        }
111    }
112
113    /// The tight service profile in salvage mode.
114    pub const fn service() -> Self {
115        Self {
116            mode: DecodeMode::Salvage,
117            limits: ResourceLimits::service(),
118        }
119    }
120}
121
122impl Default for DecodePolicy {
123    fn default() -> Self {
124        Self::desktop()
125    }
126}
127
128/// Resource options for container inspection.
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
130#[cfg_attr(feature = "schema", derive(JsonSchema))]
131#[serde(deny_unknown_fields)]
132pub struct InspectOptions {
133    /// The absolute ceilings applied during inspection.
134    pub limits: ResourceLimits,
135}
136
137pub(super) const DECOMPRESSED_TOTAL_BASE: u64 = 16 * MIB;
138pub(super) const DECOMPRESSED_TOTAL_PER_INPUT_BYTE: u64 = 1000;
139pub(super) const DECOMPRESSED_PER_EXPAND_BASE: u64 = 16 * MIB;
140pub(super) const DECOMPRESSED_PER_EXPAND_PER_INPUT_BYTE: u64 = 256;
141pub(super) const MATERIALIZED_BASE: u64 = 16 * MIB;
142pub(super) const MATERIALIZED_PER_INPUT_BYTE: u64 = 1000;
143pub(super) const RETAINED_BASE: u64 = 16 * MIB;
144pub(super) const RETAINED_PER_INPUT_BYTE: u64 = 1000;
145
146const fn default_max_materialized_bytes() -> u64 {
147    ResourceLimits::desktop().max_materialized_bytes
148}
149
150const fn default_max_retained_bytes() -> u64 {
151    ResourceLimits::desktop().max_retained_bytes
152}
153
154const fn default_max_entities() -> u64 {
155    ResourceLimits::desktop().max_entities
156}
157
158const fn default_max_collection_items() -> u64 {
159    ResourceLimits::desktop().max_collection_items
160}
161
162const fn default_max_recursion_depth() -> u64 {
163    ResourceLimits::desktop().max_recursion_depth
164}
165
166const fn default_max_work_units() -> u64 {
167    ResourceLimits::desktop().max_work_units
168}
169
170#[cfg(test)]
171mod tests {
172    #![allow(clippy::unwrap_used)]
173
174    use super::{ResourceLimits, GIB, MIB};
175
176    #[test]
177    fn desktop_ceilings_are_explicit() {
178        let d = ResourceLimits::desktop();
179        assert_eq!(d.max_input_bytes, 4 * GIB);
180        assert_eq!(d.max_decompressed_bytes_total, 8 * GIB);
181        assert_eq!(d.max_decompressed_bytes_per_expand, 2 * GIB);
182        assert_eq!(d.max_materialized_bytes, 4 * GIB);
183        assert_eq!(d.max_retained_bytes, 4 * GIB);
184        assert_eq!(d.max_entities, 64_000_000);
185        assert_eq!(d.max_collection_items, 16_000_000);
186        assert_eq!(d.max_recursion_depth, 256);
187        assert_eq!(d.max_work_units, 4_000_000_000);
188    }
189
190    #[test]
191    fn service_ceilings_are_explicit() {
192        let s = ResourceLimits::service();
193        assert_eq!(s.max_input_bytes, 256 * MIB);
194        assert_eq!(s.max_decompressed_bytes_total, GIB);
195        assert_eq!(s.max_decompressed_bytes_per_expand, 256 * MIB);
196        assert_eq!(s.max_materialized_bytes, 512 * MIB);
197        assert_eq!(s.max_retained_bytes, 512 * MIB);
198        assert_eq!(s.max_entities, 8_000_000);
199        assert_eq!(s.max_collection_items, 1_000_000);
200        assert_eq!(s.max_recursion_depth, 128);
201        assert_eq!(s.max_work_units, 256_000_000);
202    }
203
204    #[test]
205    fn added_limits_default_when_deserializing_old_policy() {
206        let json = r#"{
207            "max_input_bytes": 1,
208            "max_decompressed_bytes_total": 2,
209            "max_decompressed_bytes_per_expand": 3
210        }"#;
211        let limits: ResourceLimits = serde_json::from_str(json).unwrap();
212        let desktop = ResourceLimits::desktop();
213        assert_eq!(
214            limits.max_materialized_bytes,
215            desktop.max_materialized_bytes
216        );
217        assert_eq!(limits.max_retained_bytes, desktop.max_retained_bytes);
218        assert_eq!(limits.max_entities, desktop.max_entities);
219        assert_eq!(limits.max_collection_items, desktop.max_collection_items);
220        assert_eq!(limits.max_recursion_depth, desktop.max_recursion_depth);
221        assert_eq!(limits.max_work_units, desktop.max_work_units);
222    }
223}