termivar_scanner/plugin/
limits.rs1use serde::Serialize;
2use std::time::Duration;
3
4use super::PluginError;
5
6pub(super) const MAX_PLUGIN_ID_BYTES: usize = 128;
7pub(super) const MAX_PLUGIN_TEXT_BYTES: usize = 1024;
8pub(super) const MAX_PLUGIN_CASE_ID_BYTES: usize = 256;
9pub(super) const MAX_PLUGIN_REDACTION_LITERAL_COUNT: usize = 64;
10pub(super) const MAX_PLUGIN_REDACTION_LITERAL_BYTES: usize = 4096;
11pub(super) const MAX_PLUGIN_URL_BYTES: usize = 8192;
12pub(super) const MAX_PLUGIN_HEADERS: usize = 64;
13pub(super) const MAX_PLUGIN_HEADER_NAME_BYTES: usize = 128;
14pub(super) const MAX_PLUGIN_HEADER_VALUE_BYTES: usize = 4096;
15pub(super) const HARD_MAX_PLUGIN_INPUT_BYTES: usize = 1024 * 1024;
16pub(super) const HARD_MAX_PLUGIN_REQUESTS: u64 = 64;
17pub(super) const HARD_MAX_PLUGIN_REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
18pub(super) const HARD_MAX_PLUGIN_WALL_TIME: Duration = Duration::from_secs(300);
19pub(super) const HARD_MAX_PLUGIN_RESPONSE_BODY_BYTES: u64 = 1024 * 1024;
20pub(super) const HARD_MAX_PLUGIN_CUMULATIVE_BODY_BYTES: u64 = 4 * 1024 * 1024;
21pub(super) const HARD_MAX_PLUGIN_OBSERVATIONS: u64 = 256;
22pub(super) const HARD_MAX_PLUGIN_OBSERVATION_BYTES: u64 = 1024 * 1024;
23pub(super) const HARD_MAX_PLUGIN_TEXT_LIST_ITEMS: usize = 256;
24#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
26pub struct PluginBudget {
27 pub(super) max_input_bytes: usize,
28 pub(super) max_requests: u64,
29 pub(super) request_timeout_ms: u64,
30 pub(super) max_wall_time_ms: u64,
31 pub(super) max_response_body_bytes: u64,
32 pub(super) max_cumulative_body_bytes: u64,
33 pub(super) max_observations: u64,
34 pub(super) max_observation_bytes: u64,
35}
36
37impl Default for PluginBudget {
38 fn default() -> Self {
39 Self {
40 max_input_bytes: 64 * 1024,
41 max_requests: 16,
42 request_timeout_ms: 5_000,
43 max_wall_time_ms: 30_000,
44 max_response_body_bytes: 64 * 1024,
45 max_cumulative_body_bytes: 256 * 1024,
46 max_observations: 64,
47 max_observation_bytes: 64 * 1024,
48 }
49 }
50}
51
52impl PluginBudget {
53 pub fn with_max_input_bytes(mut self, value: usize) -> Result<Self, PluginError> {
55 if value > HARD_MAX_PLUGIN_INPUT_BYTES {
56 return Err(invalid_config(
57 "plugin input budget exceeds the hard maximum",
58 ));
59 }
60 self.max_input_bytes = value;
61 Ok(self)
62 }
63
64 pub fn with_max_requests(mut self, value: u64) -> Result<Self, PluginError> {
66 if value > HARD_MAX_PLUGIN_REQUESTS {
67 return Err(invalid_config(
68 "plugin request budget exceeds the hard maximum",
69 ));
70 }
71 self.max_requests = value;
72 Ok(self)
73 }
74
75 pub fn with_request_timeout(mut self, value: Duration) -> Result<Self, PluginError> {
77 if value > HARD_MAX_PLUGIN_REQUEST_TIMEOUT {
78 return Err(invalid_config(
79 "plugin request timeout exceeds the hard maximum",
80 ));
81 }
82 self.request_timeout_ms = duration_ms(value)?;
83 Ok(self)
84 }
85
86 pub fn with_max_wall_time(mut self, value: Duration) -> Result<Self, PluginError> {
88 if value > HARD_MAX_PLUGIN_WALL_TIME {
89 return Err(invalid_config(
90 "plugin wall budget exceeds the hard maximum",
91 ));
92 }
93 self.max_wall_time_ms = duration_ms(value)?;
94 Ok(self)
95 }
96
97 pub fn with_max_response_body_bytes(mut self, value: u64) -> Result<Self, PluginError> {
99 if value > HARD_MAX_PLUGIN_RESPONSE_BODY_BYTES {
100 return Err(invalid_config(
101 "plugin response body budget exceeds the hard maximum",
102 ));
103 }
104 self.max_response_body_bytes = value;
105 Ok(self)
106 }
107
108 pub fn with_max_cumulative_body_bytes(mut self, value: u64) -> Result<Self, PluginError> {
110 if value > HARD_MAX_PLUGIN_CUMULATIVE_BODY_BYTES {
111 return Err(invalid_config(
112 "plugin cumulative body budget exceeds the hard maximum",
113 ));
114 }
115 self.max_cumulative_body_bytes = value;
116 Ok(self)
117 }
118
119 pub fn with_max_observations(mut self, value: u64) -> Result<Self, PluginError> {
121 if value > HARD_MAX_PLUGIN_OBSERVATIONS {
122 return Err(invalid_config(
123 "plugin observation count exceeds the hard maximum",
124 ));
125 }
126 self.max_observations = value;
127 Ok(self)
128 }
129
130 pub fn with_max_observation_bytes(mut self, value: u64) -> Result<Self, PluginError> {
132 if value > HARD_MAX_PLUGIN_OBSERVATION_BYTES {
133 return Err(invalid_config(
134 "plugin observation byte budget exceeds the hard maximum",
135 ));
136 }
137 self.max_observation_bytes = value;
138 Ok(self)
139 }
140
141 pub const fn max_input_bytes(&self) -> usize {
143 self.max_input_bytes
144 }
145
146 pub const fn max_requests(&self) -> u64 {
148 self.max_requests
149 }
150
151 pub const fn request_timeout(&self) -> Duration {
153 Duration::from_millis(self.request_timeout_ms)
154 }
155
156 pub const fn max_wall_time(&self) -> Duration {
158 Duration::from_millis(self.max_wall_time_ms)
159 }
160
161 pub const fn max_response_body_bytes(&self) -> u64 {
163 self.max_response_body_bytes
164 }
165
166 pub const fn max_cumulative_body_bytes(&self) -> u64 {
168 self.max_cumulative_body_bytes
169 }
170
171 pub const fn max_observations(&self) -> u64 {
173 self.max_observations
174 }
175
176 pub const fn max_observation_bytes(&self) -> u64 {
178 self.max_observation_bytes
179 }
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
184pub struct PluginConfig {
185 pub(super) enabled: bool,
186}
187
188impl Default for PluginConfig {
189 fn default() -> Self {
190 Self { enabled: true }
191 }
192}
193
194impl PluginConfig {
195 pub fn new(enabled: bool) -> Self {
197 Self { enabled }
198 }
199
200 pub const fn enabled(&self) -> bool {
202 self.enabled
203 }
204}
205
206pub(super) fn validate_identifier(
207 value: &str,
208 field: &'static str,
209 max: usize,
210) -> Result<(), PluginError> {
211 if value.is_empty()
212 || value.len() > max
213 || !value.bytes().all(|byte| {
214 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_' | b':' | b'/')
215 })
216 {
217 return Err(invalid_config(field));
218 }
219 Ok(())
220}
221
222pub(super) fn validate_text(value: &str, field: &'static str) -> Result<(), PluginError> {
223 if value.trim().is_empty() || value.len() > MAX_PLUGIN_TEXT_BYTES {
224 return Err(invalid_config(field));
225 }
226 Ok(())
227}
228
229pub(super) fn ensure_input_budget(input: &[u8], budget: &PluginBudget) -> Result<(), PluginError> {
230 if input.len() > budget.max_input_bytes {
231 return Err(PluginError::InputBudgetExceeded {
232 actual: input.len(),
233 maximum: budget.max_input_bytes,
234 });
235 }
236 Ok(())
237}
238
239fn duration_ms(duration: Duration) -> Result<u64, PluginError> {
240 let milliseconds = u64::try_from(duration.as_millis())
241 .map_err(|_| invalid_config("plugin duration exceeds supported milliseconds"))?;
242 if !duration.is_zero() && milliseconds == 0 {
243 return Err(invalid_config(
244 "sub-millisecond plugin durations are unsupported",
245 ));
246 }
247 Ok(milliseconds)
248}
249
250pub(super) fn invalid_config(detail: &'static str) -> PluginError {
251 PluginError::InvalidConfig(detail.to_owned())
252}