Skip to main content

termivar_scanner/plugin/
limits.rs

1use 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/// Immutable invocation limits enforced by [`crate::PluginContext`].
25#[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    /// Sets the input-byte ceiling. Zero allows only empty input.
54    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    /// Sets the request ceiling. Zero grants no transport authority.
65    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    /// Sets the per-request timeout. Zero denies request dispatch.
76    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    /// Sets the invocation wall budget. Zero denies plugin execution.
87    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    /// Sets the delivered-body ceiling for one response.
98    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    /// Sets the invocation-wide delivered response body ceiling.
109    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    /// Sets the maximum number of recorded observations.
120    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    /// Sets the aggregate raw observation-value byte ceiling.
131    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    /// Maximum input bytes.
142    pub const fn max_input_bytes(&self) -> usize {
143        self.max_input_bytes
144    }
145
146    /// Maximum broker dispatches.
147    pub const fn max_requests(&self) -> u64 {
148        self.max_requests
149    }
150
151    /// Per-request timeout.
152    pub const fn request_timeout(&self) -> Duration {
153        Duration::from_millis(self.request_timeout_ms)
154    }
155
156    /// Invocation wall budget.
157    pub const fn max_wall_time(&self) -> Duration {
158        Duration::from_millis(self.max_wall_time_ms)
159    }
160
161    /// Maximum delivered bytes for one response.
162    pub const fn max_response_body_bytes(&self) -> u64 {
163        self.max_response_body_bytes
164    }
165
166    /// Maximum invocation-wide delivered response bytes.
167    pub const fn max_cumulative_body_bytes(&self) -> u64 {
168        self.max_cumulative_body_bytes
169    }
170
171    /// Maximum observation count.
172    pub const fn max_observations(&self) -> u64 {
173        self.max_observations
174    }
175
176    /// Maximum raw observation-value bytes.
177    pub const fn max_observation_bytes(&self) -> u64 {
178        self.max_observation_bytes
179    }
180}
181
182/// Host-owned registration configuration.
183#[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    /// Creates host policy with the requested enable state.
196    pub fn new(enabled: bool) -> Self {
197        Self { enabled }
198    }
199
200    /// Returns whether host policy enables the plugin.
201    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}