1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5use std::time::Duration;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub enum Capability {
18 FileRead(Vec<PathBuf>),
20 FileWrite(Vec<PathBuf>),
22 NetworkAccess(Vec<String>),
24 EnvironmentRead(Vec<String>),
26 Stdout,
28 Stderr,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct ResourceLimits {
39 pub max_memory_bytes: usize,
41 pub max_fuel: u64,
43 #[serde(with = "duration_serde")]
45 pub max_execution_time: Duration,
46}
47
48impl Default for ResourceLimits {
49 fn default() -> Self {
50 Self {
51 max_memory_bytes: 16 * 1024 * 1024, max_fuel: 1_000_000,
53 max_execution_time: Duration::from_secs(30),
54 }
55 }
56}
57
58#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
78pub struct SandboxConfig {
79 pub resource_limits: ResourceLimits,
81 pub capabilities: Vec<Capability>,
83 pub allow_host_calls: bool,
85}
86
87impl SandboxConfig {
88 pub fn new() -> Self {
90 Self::default()
91 }
92
93 pub fn with_memory_limit(mut self, bytes: usize) -> Self {
95 self.resource_limits.max_memory_bytes = bytes;
96 self
97 }
98
99 pub fn with_fuel_limit(mut self, fuel: u64) -> Self {
101 self.resource_limits.max_fuel = fuel;
102 self
103 }
104
105 pub fn with_timeout(mut self, duration: Duration) -> Self {
107 self.resource_limits.max_execution_time = duration;
108 self
109 }
110
111 pub fn with_capability(mut self, cap: Capability) -> Self {
113 self.capabilities.push(cap);
114 self
115 }
116
117 pub fn with_capabilities(mut self, caps: impl IntoIterator<Item = Capability>) -> Self {
119 self.capabilities.extend(caps);
120 self
121 }
122
123 pub fn allow_host_calls(mut self) -> Self {
125 self.allow_host_calls = true;
126 self
127 }
128}
129
130mod duration_serde {
137 use serde::{Deserialize, Deserializer, Serialize, Serializer};
138 use std::time::Duration;
139
140 #[derive(Serialize, Deserialize)]
141 struct DurationRepr {
142 secs: u64,
143 nanos: u32,
144 }
145
146 pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
147 where
148 S: Serializer,
149 {
150 let repr = DurationRepr {
151 secs: duration.as_secs(),
152 nanos: duration.subsec_nanos(),
153 };
154 repr.serialize(serializer)
155 }
156
157 pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
158 where
159 D: Deserializer<'de>,
160 {
161 let repr = DurationRepr::deserialize(deserializer)?;
162 Ok(Duration::new(repr.secs, repr.nanos))
163 }
164}
165
166#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
177 fn test_sandbox_config_default_values() {
178 let config = SandboxConfig::default();
179
180 assert_eq!(config.resource_limits.max_memory_bytes, 16 * 1024 * 1024,);
181 assert_eq!(config.resource_limits.max_fuel, 1_000_000);
182 assert_eq!(
183 config.resource_limits.max_execution_time,
184 Duration::from_secs(30),
185 );
186 assert!(config.capabilities.is_empty());
187 assert!(!config.allow_host_calls);
188 }
189
190 #[test]
191 fn test_sandbox_config_new_equals_default() {
192 assert_eq!(SandboxConfig::new(), SandboxConfig::default());
193 }
194
195 #[test]
196 fn test_resource_limits_default() {
197 let limits = ResourceLimits::default();
198
199 assert_eq!(limits.max_memory_bytes, 16 * 1024 * 1024);
200 assert_eq!(limits.max_fuel, 1_000_000);
201 assert_eq!(limits.max_execution_time, Duration::from_secs(30));
202 }
203
204 #[test]
207 fn test_builder_chain() {
208 let config = SandboxConfig::new()
209 .with_memory_limit(32 * 1024 * 1024)
210 .with_fuel_limit(2_000_000)
211 .with_timeout(Duration::from_secs(60))
212 .with_capability(Capability::Stdout)
213 .with_capability(Capability::Stderr)
214 .allow_host_calls();
215
216 assert_eq!(config.resource_limits.max_memory_bytes, 32 * 1024 * 1024);
217 assert_eq!(config.resource_limits.max_fuel, 2_000_000);
218 assert_eq!(
219 config.resource_limits.max_execution_time,
220 Duration::from_secs(60),
221 );
222 assert_eq!(config.capabilities.len(), 2);
223 assert!(config.allow_host_calls);
224 }
225
226 #[test]
227 fn test_builder_with_capabilities_batch() {
228 let caps = vec![
229 Capability::Stdout,
230 Capability::Stderr,
231 Capability::NetworkAccess(vec!["localhost".to_string()]),
232 ];
233
234 let config = SandboxConfig::new().with_capabilities(caps);
235
236 assert_eq!(config.capabilities.len(), 3);
237 }
238
239 #[test]
240 fn test_builder_with_memory_limit() {
241 let config = SandboxConfig::new().with_memory_limit(64 * 1024 * 1024);
242 assert_eq!(config.resource_limits.max_memory_bytes, 64 * 1024 * 1024);
243 assert_eq!(config.resource_limits.max_fuel, 1_000_000);
245 }
246
247 #[test]
248 fn test_builder_allow_host_calls() {
249 let config = SandboxConfig::new();
250 assert!(!config.allow_host_calls);
251
252 let config = config.allow_host_calls();
253 assert!(config.allow_host_calls);
254 }
255
256 #[test]
259 fn test_capability_file_read() {
260 let cap = Capability::FileRead(vec![
261 PathBuf::from("/tmp/data"),
262 PathBuf::from("/home/user/docs"),
263 ]);
264
265 if let Capability::FileRead(paths) = &cap {
266 assert_eq!(paths.len(), 2);
267 assert_eq!(paths[0], PathBuf::from("/tmp/data"));
268 } else {
269 panic!("expected FileRead variant");
270 }
271 }
272
273 #[test]
274 fn test_config_with_various_capabilities() {
275 let config = SandboxConfig::new()
276 .with_capability(Capability::FileRead(vec![PathBuf::from("/data")]))
277 .with_capability(Capability::FileWrite(vec![PathBuf::from("/output")]))
278 .with_capability(Capability::NetworkAccess(vec![
279 "api.example.com".to_string(),
280 ]))
281 .with_capability(Capability::EnvironmentRead(vec![
282 "HOME".to_string(),
283 "PATH".to_string(),
284 ]))
285 .with_capability(Capability::Stdout)
286 .with_capability(Capability::Stderr);
287
288 assert_eq!(config.capabilities.len(), 6);
289 }
290
291 #[test]
294 fn test_sandbox_config_serde_round_trip() {
295 let config = SandboxConfig::new()
296 .with_memory_limit(8 * 1024 * 1024)
297 .with_fuel_limit(500_000)
298 .with_timeout(Duration::from_secs(10))
299 .with_capability(Capability::Stdout)
300 .with_capability(Capability::FileRead(vec![PathBuf::from("/tmp")]))
301 .allow_host_calls();
302
303 let json = serde_json::to_string(&config).unwrap();
304 let decoded: SandboxConfig = serde_json::from_str(&json).unwrap();
305
306 assert_eq!(config, decoded);
307 }
308
309 #[test]
310 fn test_resource_limits_serde_round_trip() {
311 let limits = ResourceLimits {
312 max_memory_bytes: 4 * 1024 * 1024,
313 max_fuel: 250_000,
314 max_execution_time: Duration::from_millis(1500),
315 };
316
317 let json = serde_json::to_string(&limits).unwrap();
318 let decoded: ResourceLimits = serde_json::from_str(&json).unwrap();
319
320 assert_eq!(limits, decoded);
321 }
322
323 #[test]
324 fn test_capability_serde_round_trip() {
325 let caps = vec![
326 Capability::FileRead(vec![PathBuf::from("/a"), PathBuf::from("/b")]),
327 Capability::FileWrite(vec![PathBuf::from("/c")]),
328 Capability::NetworkAccess(vec!["example.com".to_string()]),
329 Capability::EnvironmentRead(vec!["HOME".to_string()]),
330 Capability::Stdout,
331 Capability::Stderr,
332 ];
333
334 let json = serde_json::to_string(&caps).unwrap();
335 let decoded: Vec<Capability> = serde_json::from_str(&json).unwrap();
336
337 assert_eq!(caps, decoded);
338 }
339
340 #[test]
341 fn test_default_config_serde_round_trip() {
342 let config = SandboxConfig::default();
343 let json = serde_json::to_string(&config).unwrap();
344 let decoded: SandboxConfig = serde_json::from_str(&json).unwrap();
345
346 assert_eq!(config, decoded);
347 }
348
349 #[test]
350 fn test_duration_json_shape() {
351 let limits = ResourceLimits::default();
352 let value: serde_json::Value = serde_json::to_value(&limits).unwrap();
353
354 let time = value.get("max_execution_time").unwrap();
356 assert_eq!(time.get("secs").unwrap(), 30);
357 assert_eq!(time.get("nanos").unwrap(), 0);
358 }
359}