1#[cfg(feature = "schema")]
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8const MIB: u64 = 1024 * 1024;
9const GIB: u64 = 1024 * MIB;
10
11#[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 Strict,
19 #[default]
21 Salvage,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[cfg_attr(feature = "schema", derive(JsonSchema))]
27#[serde(deny_unknown_fields)]
28pub struct ResourceLimits {
29 pub max_input_bytes: u64,
31 pub max_decompressed_bytes_total: u64,
33 pub max_decompressed_bytes_per_expand: u64,
35 #[serde(default = "default_max_retained_bytes")]
38 pub max_retained_bytes: u64,
39 #[serde(default = "default_max_entities")]
41 pub max_entities: u64,
42 #[serde(default = "default_max_recursion_depth")]
44 pub max_recursion_depth: u64,
45}
46
47impl ResourceLimits {
48 pub const fn desktop() -> Self {
50 Self {
51 max_input_bytes: 4 * GIB,
52 max_decompressed_bytes_total: 8 * GIB,
53 max_decompressed_bytes_per_expand: 2 * GIB,
54 max_retained_bytes: 4 * GIB,
55 max_entities: 64_000_000,
56 max_recursion_depth: 256,
57 }
58 }
59
60 pub const fn service() -> Self {
62 Self {
63 max_input_bytes: 256 * MIB,
64 max_decompressed_bytes_total: GIB,
65 max_decompressed_bytes_per_expand: 256 * MIB,
66 max_retained_bytes: 512 * MIB,
67 max_entities: 8_000_000,
68 max_recursion_depth: 128,
69 }
70 }
71}
72
73impl Default for ResourceLimits {
74 fn default() -> Self {
75 Self::desktop()
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
81#[cfg_attr(feature = "schema", derive(JsonSchema))]
82#[serde(deny_unknown_fields)]
83pub struct DecodePolicy {
84 pub mode: DecodeMode,
86 pub limits: ResourceLimits,
88}
89
90impl DecodePolicy {
91 pub const fn desktop() -> Self {
93 Self {
94 mode: DecodeMode::Salvage,
95 limits: ResourceLimits::desktop(),
96 }
97 }
98
99 pub const fn service() -> Self {
101 Self {
102 mode: DecodeMode::Salvage,
103 limits: ResourceLimits::service(),
104 }
105 }
106}
107
108impl Default for DecodePolicy {
109 fn default() -> Self {
110 Self::desktop()
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
116#[cfg_attr(feature = "schema", derive(JsonSchema))]
117#[serde(deny_unknown_fields)]
118pub struct InspectOptions {
119 pub limits: ResourceLimits,
121}
122
123pub(super) const DECOMPRESSED_TOTAL_BASE: u64 = 16 * MIB;
124pub(super) const DECOMPRESSED_TOTAL_PER_INPUT_BYTE: u64 = 1000;
125pub(super) const DECOMPRESSED_PER_EXPAND_BASE: u64 = 16 * MIB;
126pub(super) const DECOMPRESSED_PER_EXPAND_PER_INPUT_BYTE: u64 = 256;
127pub(super) const RETAINED_BASE: u64 = 16 * MIB;
128pub(super) const RETAINED_PER_INPUT_BYTE: u64 = 1000;
129
130const fn default_max_retained_bytes() -> u64 {
131 ResourceLimits::desktop().max_retained_bytes
132}
133
134const fn default_max_entities() -> u64 {
135 ResourceLimits::desktop().max_entities
136}
137
138const fn default_max_recursion_depth() -> u64 {
139 ResourceLimits::desktop().max_recursion_depth
140}
141
142#[cfg(test)]
143mod tests {
144 #![allow(clippy::unwrap_used)]
145
146 use super::{DecodePolicy, ResourceLimits, GIB, MIB};
147
148 #[test]
149 fn desktop_ceilings_are_explicit() {
150 let d = ResourceLimits::desktop();
151 assert_eq!(d.max_input_bytes, 4 * GIB);
152 assert_eq!(d.max_decompressed_bytes_total, 8 * GIB);
153 assert_eq!(d.max_decompressed_bytes_per_expand, 2 * GIB);
154 assert_eq!(d.max_retained_bytes, 4 * GIB);
155 assert_eq!(d.max_entities, 64_000_000);
156 assert_eq!(d.max_recursion_depth, 256);
157 }
158
159 #[test]
160 fn service_ceilings_are_explicit() {
161 let s = ResourceLimits::service();
162 assert_eq!(s.max_input_bytes, 256 * MIB);
163 assert_eq!(s.max_decompressed_bytes_total, GIB);
164 assert_eq!(s.max_decompressed_bytes_per_expand, 256 * MIB);
165 assert_eq!(s.max_retained_bytes, 512 * MIB);
166 assert_eq!(s.max_entities, 8_000_000);
167 assert_eq!(s.max_recursion_depth, 128);
168 }
169
170 #[test]
171 fn added_limits_default_when_deserializing_old_policy() {
172 let json = r#"{
173 "max_input_bytes": 1,
174 "max_decompressed_bytes_total": 2,
175 "max_decompressed_bytes_per_expand": 3
176 }"#;
177 let limits: ResourceLimits = serde_json::from_str(json).unwrap();
178 let desktop = ResourceLimits::desktop();
179 assert_eq!(limits.max_retained_bytes, desktop.max_retained_bytes);
180 assert_eq!(limits.max_entities, desktop.max_entities);
181 assert_eq!(limits.max_recursion_depth, desktop.max_recursion_depth);
182 }
183
184 #[test]
185 fn removed_limits_are_rejected_by_name() {
186 for field in [
187 "max_materialized_bytes",
188 "max_collection_items",
189 "max_work_units",
190 ] {
191 let limits = format!(
192 r#"{{"max_input_bytes":1,"max_decompressed_bytes_total":2,"max_decompressed_bytes_per_expand":3,"{field}":4}}"#
193 );
194 let policy = format!(r#"{{"mode":"salvage","limits":{limits}}}"#);
195 let expected = format!("unknown field `{field}`");
196 let error = serde_json::from_str::<ResourceLimits>(&limits).unwrap_err();
197 assert!(error.to_string().contains(&expected), "{error}");
198 let error = serde_json::from_str::<DecodePolicy>(&policy).unwrap_err();
199 assert!(error.to_string().contains(&expected), "{error}");
200 }
201 }
202}