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