1use std::collections::{HashMap, HashSet};
41use std::path::PathBuf;
42use std::time::Duration;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum NotifyMode {
49 #[default]
51 Disabled,
52 Monitor,
55 Virtualize,
58}
59
60#[derive(Debug, Clone)]
64pub struct Mount {
65 pub source: PathBuf,
67 pub target: PathBuf,
69 pub writable: bool,
71 pub executable: bool,
73}
74
75impl Mount {
76 pub fn ro(path: impl Into<PathBuf>) -> Self {
78 let path = path.into();
79 Self {
80 source: path.clone(),
81 target: path,
82 writable: false,
83 executable: true,
84 }
85 }
86
87 pub fn ro_noexec(path: impl Into<PathBuf>) -> Self {
89 let path = path.into();
90 Self {
91 source: path.clone(),
92 target: path,
93 writable: false,
94 executable: false,
95 }
96 }
97
98 pub fn rw(path: impl Into<PathBuf>) -> Self {
100 let path = path.into();
101 Self {
102 source: path.clone(),
103 target: path,
104 writable: true,
105 executable: true,
106 }
107 }
108
109 pub fn bind(source: impl Into<PathBuf>, target: impl Into<PathBuf>) -> Self {
111 Self {
112 source: source.into(),
113 target: target.into(),
114 writable: false,
115 executable: true,
116 }
117 }
118
119 pub fn writable(mut self) -> Self {
121 self.writable = true;
122 self
123 }
124
125 pub fn noexec(mut self) -> Self {
127 self.executable = false;
128 self
129 }
130}
131
132#[derive(Debug, Clone, Default)]
152pub struct Syscalls {
153 pub allowed: HashSet<i64>,
155 pub denied: HashSet<i64>,
157}
158
159impl Syscalls {
160 pub fn new() -> Self {
162 Self::default()
163 }
164
165 pub fn allow(mut self, syscall: i64) -> Self {
167 self.allowed.insert(syscall);
168 self.denied.remove(&syscall);
169 self
170 }
171
172 pub fn deny(mut self, syscall: i64) -> Self {
174 self.denied.insert(syscall);
175 self.allowed.remove(&syscall);
176 self
177 }
178
179 pub fn allow_many(mut self, syscalls: impl IntoIterator<Item = i64>) -> Self {
181 for syscall in syscalls {
182 self.allowed.insert(syscall);
183 self.denied.remove(&syscall);
184 }
185 self
186 }
187
188 pub fn deny_many(mut self, syscalls: impl IntoIterator<Item = i64>) -> Self {
190 for syscall in syscalls {
191 self.denied.insert(syscall);
192 self.allowed.remove(&syscall);
193 }
194 self
195 }
196}
197
198#[derive(Debug, Clone, Default)]
214pub struct Landlock {
215 pub read_paths: Vec<PathBuf>,
217 pub write_paths: Vec<PathBuf>,
219 pub execute_paths: Vec<PathBuf>,
221}
222
223impl Landlock {
224 pub fn new() -> Self {
226 Self::default()
227 }
228
229 pub fn allow_read(mut self, path: impl Into<PathBuf>) -> Self {
231 self.read_paths.push(path.into());
232 self
233 }
234
235 pub fn allow_read_write(mut self, path: impl Into<PathBuf>) -> Self {
237 self.write_paths.push(path.into());
238 self
239 }
240
241 pub fn allow_execute(mut self, path: impl Into<PathBuf>) -> Self {
243 self.execute_paths.push(path.into());
244 self
245 }
246}
247
248#[derive(Debug, Clone)]
250pub struct UserFile {
251 pub path: String,
252 pub content: Vec<u8>,
253 pub executable: bool,
254}
255
256impl UserFile {
257 pub fn new(path: impl Into<String>, content: impl Into<Vec<u8>>) -> Self {
258 Self {
259 path: path.into(),
260 content: content.into(),
261 executable: false,
262 }
263 }
264
265 pub fn executable(mut self) -> Self {
266 self.executable = true;
267 self
268 }
269}
270
271#[must_use]
290#[derive(Debug, Clone)]
291pub struct Plan {
292 pub cmd: Vec<String>,
293 pub binary_path: Option<PathBuf>,
296 pub env: HashMap<String, String>,
297 pub stdin: Option<Vec<u8>>,
298 pub cwd: String,
299 pub mounts: Vec<Mount>,
300 pub user_files: Vec<UserFile>,
301 pub workspace_size: u64,
302 pub timeout: Duration,
303 pub memory_limit: u64,
304 pub max_pids: u32,
305 pub max_output: u64,
306 pub network_blocked: bool,
307 pub syscalls: Option<Syscalls>,
309 pub landlock: Option<Landlock>,
311 pub notify_mode: NotifyMode,
313}
314
315impl Default for Plan {
316 fn default() -> Self {
317 Self {
318 cmd: Vec::new(),
319 binary_path: None,
320 env: default_env(),
321 stdin: None,
322 cwd: "/work".into(),
323 mounts: Vec::new(),
324 user_files: Vec::new(),
325 workspace_size: 64 * 1024 * 1024,
326 timeout: Duration::from_secs(30),
327 memory_limit: 256 * 1024 * 1024,
328 max_pids: 64,
329 max_output: 16 * 1024 * 1024,
330 network_blocked: true,
331 syscalls: None,
332 landlock: None,
333 notify_mode: NotifyMode::Disabled,
334 }
335 }
336}
337
338impl Plan {
339 pub fn new(cmd: impl IntoIterator<Item = impl Into<String>>) -> Self {
340 Self {
341 cmd: cmd.into_iter().map(Into::into).collect(),
342 ..Default::default()
343 }
344 }
345
346 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
347 self.env.insert(key.into(), value.into());
348 self
349 }
350
351 pub fn stdin(mut self, data: impl Into<Vec<u8>>) -> Self {
352 self.stdin = Some(data.into());
353 self
354 }
355
356 pub fn cwd(mut self, cwd: impl Into<String>) -> Self {
357 self.cwd = cwd.into();
358 self
359 }
360
361 pub fn mount(mut self, mount: Mount) -> Self {
362 self.mounts.push(mount);
363 self
364 }
365
366 pub fn mounts(mut self, mounts: impl IntoIterator<Item = Mount>) -> Self {
368 self.mounts.extend(mounts);
369 self
370 }
371
372 pub fn binary_path(mut self, path: impl Into<PathBuf>) -> Self {
377 self.binary_path = Some(path.into());
378 self
379 }
380
381 pub fn file(mut self, path: impl Into<String>, content: impl Into<Vec<u8>>) -> Self {
382 self.user_files.push(UserFile::new(path, content));
383 self
384 }
385
386 pub fn executable(mut self, path: impl Into<String>, content: impl Into<Vec<u8>>) -> Self {
388 self.user_files
389 .push(UserFile::new(path, content).executable());
390 self
391 }
392
393 pub fn timeout(mut self, timeout: Duration) -> Self {
394 self.timeout = timeout;
395 self
396 }
397
398 pub fn memory_limit(mut self, limit: u64) -> Self {
399 self.memory_limit = limit;
400 self
401 }
402
403 pub fn max_pids(mut self, max: u32) -> Self {
404 self.max_pids = max;
405 self
406 }
407
408 pub fn max_output(mut self, max: u64) -> Self {
409 self.max_output = max;
410 self
411 }
412
413 pub fn network_blocked(mut self, blocked: bool) -> Self {
414 self.network_blocked = blocked;
415 self
416 }
417
418 pub fn network(mut self, enabled: bool) -> Self {
423 self.network_blocked = !enabled;
424 self
425 }
426
427 pub fn memory(self, limit: u64) -> Self {
429 self.memory_limit(limit)
430 }
431
432 pub fn syscalls(mut self, syscalls: Syscalls) -> Self {
434 self.syscalls = Some(syscalls);
435 self
436 }
437
438 pub fn landlock(mut self, landlock: Landlock) -> Self {
440 self.landlock = Some(landlock);
441 self
442 }
443
444 pub fn notify_mode(mut self, mode: NotifyMode) -> Self {
450 self.notify_mode = mode;
451 self
452 }
453
454 pub fn exec(self) -> Result<crate::Output, crate::ExecutorError> {
458 crate::Executor::run(self)
459 }
460}
461
462fn default_env() -> HashMap<String, String> {
463 let default_path = if std::path::Path::new("/nix/store").exists() {
465 "/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin:/usr/local/bin:/usr/bin:/bin"
466 } else {
467 "/usr/local/bin:/usr/bin:/bin"
468 };
469
470 HashMap::from([
471 ("PATH".into(), default_path.into()),
472 ("HOME".into(), "/home".into()),
473 ("USER".into(), "sandbox".into()),
474 ("LANG".into(), "C.UTF-8".into()),
475 ("LC_ALL".into(), "C.UTF-8".into()),
476 ])
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 #[test]
484 fn plan_new() {
485 let plan = Plan::new(["echo", "hello"]);
486 assert_eq!(plan.cmd, vec!["echo", "hello"]);
487 assert!(plan.network_blocked);
488 }
489
490 #[test]
491 fn plan_builder() {
492 let plan = Plan::new(["python", "main.py"])
493 .env("PYTHONPATH", "/work")
494 .stdin(b"input".to_vec())
495 .timeout(Duration::from_secs(10))
496 .file("main.py", b"print('hello')");
497
498 assert_eq!(plan.env.get("PYTHONPATH"), Some(&"/work".into()));
499 assert_eq!(plan.stdin, Some(b"input".to_vec()));
500 assert_eq!(plan.timeout, Duration::from_secs(10));
501 assert_eq!(plan.user_files.len(), 1);
502 }
503
504 #[test]
505 fn plan_network_methods() {
506 let plan = Plan::new(["echo"]).network(true);
507 assert!(!plan.network_blocked);
508
509 let plan = Plan::new(["echo"]).network(false);
510 assert!(plan.network_blocked);
511 }
512
513 #[test]
514 fn plan_syscalls_config() {
515 let syscalls = Syscalls::default().allow(1).allow(2).deny(3);
516
517 assert!(syscalls.allowed.contains(&1));
518 assert!(syscalls.allowed.contains(&2));
519 assert!(syscalls.denied.contains(&3));
520 }
521
522 #[test]
523 fn plan_landlock_config() {
524 let landlock = Landlock::new().allow_read("/etc").allow_read_write("/tmp");
525
526 assert_eq!(landlock.read_paths.len(), 1);
527 assert_eq!(landlock.write_paths.len(), 1);
528 }
529}