Skip to main content

mlua_batteries/
config.rs

1//! Runtime configuration for mlua-batteries modules.
2//!
3//! Use [`Config::builder`] to customise behaviour, or
4//! [`Config::default`] for unrestricted defaults.
5//!
6//! ```rust,ignore
7//! // Requires the `sandbox` feature.
8//! use std::time::Duration;
9//! use mlua_batteries::config::Config;
10//! use mlua_batteries::policy::Sandboxed;
11//!
12//! let config = Config::builder()
13//!     .path_policy(Sandboxed::new(["/app/data"]).unwrap())
14//!     .max_walk_depth(50)
15//!     .http_timeout(Duration::from_secs(60))
16//!     .build()
17//!     .expect("invalid config");
18//! ```
19
20use std::time::Duration;
21
22use crate::policy::{EnvPolicy, HttpPolicy, LlmPolicy, PathPolicy, Unrestricted};
23
24/// Error returned by [`ConfigBuilder::build`] for invalid configuration values.
25#[derive(Debug, Clone)]
26pub struct ConfigError(String);
27
28impl ConfigError {
29    /// Create a new configuration error.
30    pub fn new(message: impl Into<String>) -> Self {
31        Self(message.into())
32    }
33
34    /// The error message.
35    pub fn message(&self) -> &str {
36        &self.0
37    }
38}
39
40impl std::fmt::Display for ConfigError {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        f.write_str(&self.0)
43    }
44}
45
46impl std::error::Error for ConfigError {}
47
48/// Central configuration for all mlua-batteries modules.
49///
50/// Contains trait-object policies and numeric limits. `Debug` prints
51/// the numeric limits (policy trait objects are omitted).
52pub struct Config {
53    pub(crate) path_policy: Box<dyn PathPolicy>,
54    pub(crate) http_policy: Box<dyn HttpPolicy>,
55    pub(crate) env_policy: Box<dyn EnvPolicy>,
56    pub(crate) llm_policy: Box<dyn LlmPolicy>,
57    pub(crate) max_read_bytes: Option<u64>,
58    pub(crate) max_walk_depth: usize,
59    pub(crate) max_walk_entries: usize,
60    pub(crate) max_json_depth: usize,
61    pub(crate) http_timeout: Duration,
62    pub(crate) max_response_bytes: u64,
63    pub(crate) max_sleep_secs: f64,
64    pub(crate) llm_default_timeout_secs: u64,
65    pub(crate) llm_max_response_bytes: u64,
66    pub(crate) llm_max_batch_concurrency: usize,
67}
68
69impl std::fmt::Debug for Config {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        f.debug_struct("Config")
72            .field("path_policy", &self.path_policy.policy_name())
73            .field("http_policy", &self.http_policy.policy_name())
74            .field("env_policy", &self.env_policy.policy_name())
75            .field("llm_policy", &self.llm_policy.policy_name())
76            .field("max_read_bytes", &self.max_read_bytes)
77            .field("max_walk_depth", &self.max_walk_depth)
78            .field("max_walk_entries", &self.max_walk_entries)
79            .field("max_json_depth", &self.max_json_depth)
80            .field("http_timeout", &self.http_timeout)
81            .field("max_response_bytes", &self.max_response_bytes)
82            .field("max_sleep_secs", &self.max_sleep_secs)
83            .field("llm_default_timeout_secs", &self.llm_default_timeout_secs)
84            .field("llm_max_response_bytes", &self.llm_max_response_bytes)
85            .field("llm_max_batch_concurrency", &self.llm_max_batch_concurrency)
86            .finish()
87    }
88}
89
90impl Default for Config {
91    fn default() -> Self {
92        Self {
93            path_policy: Box::new(Unrestricted),
94            http_policy: Box::new(Unrestricted),
95            env_policy: Box::new(Unrestricted),
96            llm_policy: Box::new(Unrestricted),
97            max_read_bytes: None,
98            max_walk_depth: 256,
99            max_walk_entries: 10_000,
100            max_json_depth: 128,
101            http_timeout: Duration::from_secs(30),
102            max_response_bytes: 10 * 1024 * 1024, // 10 MiB
103            max_sleep_secs: 86_400.0,
104            llm_default_timeout_secs: 120,
105            llm_max_response_bytes: 10 * 1024 * 1024, // 10 MiB
106            llm_max_batch_concurrency: 8,
107        }
108    }
109}
110
111impl Config {
112    /// Start building a custom configuration.
113    pub fn builder() -> ConfigBuilder {
114        ConfigBuilder {
115            inner: Config::default(),
116        }
117    }
118}
119
120/// Builder for [`Config`].
121pub struct ConfigBuilder {
122    inner: Config,
123}
124
125impl ConfigBuilder {
126    /// Set the path access policy.
127    ///
128    /// Default: [`Unrestricted`] (no checks).
129    pub fn path_policy(mut self, policy: impl PathPolicy) -> Self {
130        self.inner.path_policy = Box::new(policy);
131        self
132    }
133
134    /// Set the HTTP URL access policy.
135    ///
136    /// Default: [`Unrestricted`] (no checks).
137    pub fn http_policy(mut self, policy: impl HttpPolicy) -> Self {
138        self.inner.http_policy = Box::new(policy);
139        self
140    }
141
142    /// Set the environment variable access policy.
143    ///
144    /// Default: [`Unrestricted`] (no checks).
145    pub fn env_policy(mut self, policy: impl EnvPolicy) -> Self {
146        self.inner.env_policy = Box::new(policy);
147        self
148    }
149
150    /// Set the LLM request policy.
151    ///
152    /// Default: [`Unrestricted`] (no checks).
153    pub fn llm_policy(mut self, policy: impl LlmPolicy) -> Self {
154        self.inner.llm_policy = Box::new(policy);
155        self
156    }
157
158    /// Default timeout for LLM requests in seconds.
159    ///
160    /// Default: `120`.
161    pub fn llm_default_timeout_secs(mut self, secs: u64) -> Self {
162        self.inner.llm_default_timeout_secs = secs;
163        self
164    }
165
166    /// Maximum LLM response body size in bytes.
167    ///
168    /// Default: `10_485_760` (10 MiB).
169    pub fn llm_max_response_bytes(mut self, bytes: u64) -> Self {
170        self.inner.llm_max_response_bytes = bytes;
171        self
172    }
173
174    /// Maximum number of concurrent threads for `llm.batch`.
175    ///
176    /// Default: `8`.
177    pub fn llm_max_batch_concurrency(mut self, n: usize) -> Self {
178        self.inner.llm_max_batch_concurrency = n;
179        self
180    }
181
182    /// Maximum number of bytes `fs.read` and `fs.read_binary` will load.
183    ///
184    /// Default: `None` (no limit).  When set, an error is returned if
185    /// the file size exceeds this value.
186    pub fn max_read_bytes(mut self, bytes: u64) -> Self {
187        self.inner.max_read_bytes = Some(bytes);
188        self
189    }
190
191    /// Maximum directory depth for `fs.walk`.
192    ///
193    /// Default: `256`.
194    pub fn max_walk_depth(mut self, depth: usize) -> Self {
195        self.inner.max_walk_depth = depth;
196        self
197    }
198
199    /// Maximum number of entries returned by `fs.walk` and `fs.glob`.
200    ///
201    /// Default: `10_000`.
202    pub fn max_walk_entries(mut self, entries: usize) -> Self {
203        self.inner.max_walk_entries = entries;
204        self
205    }
206
207    /// Maximum nesting depth for JSON encode/decode.
208    ///
209    /// Default: `128`.
210    pub fn max_json_depth(mut self, depth: usize) -> Self {
211        self.inner.max_json_depth = depth;
212        self
213    }
214
215    /// Default timeout for HTTP requests.
216    ///
217    /// Default: `30` seconds.
218    pub fn http_timeout(mut self, timeout: Duration) -> Self {
219        self.inner.http_timeout = timeout;
220        self
221    }
222
223    /// Maximum HTTP response body size in bytes.
224    ///
225    /// Default: `10_485_760` (10 MiB).
226    pub fn max_response_bytes(mut self, bytes: u64) -> Self {
227        self.inner.max_response_bytes = bytes;
228        self
229    }
230
231    /// Maximum duration for `time.sleep` in seconds.
232    ///
233    /// Default: `86400.0` (1 day).
234    ///
235    /// [`build`](ConfigBuilder::build) returns an error if the value is
236    /// NaN, infinite, or negative.
237    pub fn max_sleep_secs(mut self, secs: f64) -> Self {
238        self.inner.max_sleep_secs = secs;
239        self
240    }
241
242    /// Finalise the configuration.
243    ///
244    /// Returns `Err` if any configured value is invalid.
245    pub fn build(self) -> Result<Config, ConfigError> {
246        let secs = self.inner.max_sleep_secs;
247        if !secs.is_finite() || secs < 0.0 {
248            return Err(ConfigError::new(format!(
249                "max_sleep_secs must be finite and non-negative, got {secs}"
250            )));
251        }
252        Ok(self.inner)
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    #[cfg(feature = "sandbox")]
260    use crate::policy::Sandboxed;
261
262    #[test]
263    fn default_config_values() {
264        let config = Config::default();
265        assert_eq!(config.max_read_bytes, None);
266        assert_eq!(config.max_walk_depth, 256);
267        assert_eq!(config.max_walk_entries, 10_000);
268        assert_eq!(config.max_json_depth, 128);
269        assert_eq!(config.http_timeout, Duration::from_secs(30));
270        assert_eq!(config.max_response_bytes, 10 * 1024 * 1024);
271        assert!((config.max_sleep_secs - 86_400.0).abs() < f64::EPSILON);
272        assert_eq!(config.llm_default_timeout_secs, 120);
273        assert_eq!(config.llm_max_response_bytes, 10 * 1024 * 1024);
274        assert_eq!(config.llm_max_batch_concurrency, 8);
275    }
276
277    #[test]
278    fn builder_overrides() {
279        let config = Config::builder()
280            .max_read_bytes(4096)
281            .max_walk_depth(10)
282            .max_walk_entries(500)
283            .max_json_depth(32)
284            .http_timeout(Duration::from_secs(5))
285            .max_response_bytes(1024)
286            .max_sleep_secs(60.0)
287            .build()
288            .unwrap();
289
290        assert_eq!(config.max_read_bytes, Some(4096));
291        assert_eq!(config.max_walk_depth, 10);
292        assert_eq!(config.max_walk_entries, 500);
293        assert_eq!(config.max_json_depth, 32);
294        assert_eq!(config.http_timeout, Duration::from_secs(5));
295        assert_eq!(config.max_response_bytes, 1024);
296        assert!((config.max_sleep_secs - 60.0).abs() < f64::EPSILON);
297    }
298
299    #[cfg(feature = "sandbox")]
300    #[test]
301    fn builder_accepts_custom_policy() {
302        let config = Config::builder()
303            .path_policy(Sandboxed::new(["/tmp"]).unwrap())
304            .build()
305            .unwrap();
306
307        // Verify it compiles and builds — policy behaviour tested in policy.rs
308        assert_eq!(config.max_walk_depth, 256); // other defaults preserved
309    }
310
311    #[test]
312    fn builder_rejects_nan_sleep() {
313        let result = Config::builder().max_sleep_secs(f64::NAN).build();
314        assert!(result.is_err());
315        assert!(result
316            .unwrap_err()
317            .to_string()
318            .contains("max_sleep_secs must be finite and non-negative"));
319    }
320
321    #[test]
322    fn builder_rejects_infinite_sleep() {
323        let result = Config::builder().max_sleep_secs(f64::INFINITY).build();
324        assert!(result.is_err());
325    }
326
327    #[test]
328    fn builder_rejects_negative_sleep() {
329        let result = Config::builder().max_sleep_secs(-1.0).build();
330        assert!(result.is_err());
331    }
332
333    #[test]
334    fn config_debug_does_not_panic() {
335        let config = Config::default();
336        let s = format!("{config:?}");
337        assert!(s.contains("max_walk_depth"));
338        assert!(
339            s.contains("Unrestricted"),
340            "Debug should show policy type names, got: {s}"
341        );
342    }
343}