1pub mod config;
2pub mod error;
3pub mod exec;
4pub mod fs;
5pub mod runtime;
6pub mod toolbox;
7
8use std::sync::Arc;
9
10use tokio::sync::Mutex;
11
12use crate::config::SandboxConfig;
13use crate::error::{Result, SandboxError};
14use crate::fs::overlay::{FsChange, FsOverlay};
15use crate::runtime::{ExecResult, WasiRuntime};
16
17pub struct Sandbox {
19 runtime: WasiRuntime,
20 overlay: Arc<Mutex<Option<FsOverlay>>>,
21 config: SandboxConfig,
22 destroyed: Arc<std::sync::atomic::AtomicBool>,
23}
24
25impl Sandbox {
26 pub fn new(config: SandboxConfig) -> Result<Self> {
28 let overlay = FsOverlay::new(&config.work_dir)?;
29 let runtime = WasiRuntime::new(config.clone())?;
30
31 Ok(Self {
32 runtime,
33 overlay: Arc::new(Mutex::new(Some(overlay))),
34 config,
35 destroyed: Arc::new(std::sync::atomic::AtomicBool::new(false)),
36 })
37 }
38
39 pub async fn exec(&self, command: &str, args: &[String]) -> Result<ExecResult> {
41 self.check_destroyed()?;
42
43 if !toolbox::is_available(command) {
44 return Err(SandboxError::CommandNotFound(command.to_string()));
45 }
46
47 self.runtime.exec(command, args).await
48 }
49
50 pub async fn read_file(&self, path: &str) -> Result<Vec<u8>> {
52 self.check_destroyed()?;
53
54 let full_path = fs::validate_path(&self.config.work_dir, path)?;
55 let content = tokio::fs::read(&full_path).await?;
56 Ok(content)
57 }
58
59 pub async fn write_file(&self, path: &str, contents: &[u8]) -> Result<()> {
61 self.check_destroyed()?;
62
63 let full_path = fs::validate_path(&self.config.work_dir, path)?;
64
65 if let Some(parent) = full_path.parent() {
67 tokio::fs::create_dir_all(parent).await?;
68 }
69
70 tokio::fs::write(&full_path, contents).await?;
71 Ok(())
72 }
73
74 pub async fn list_dir(&self, path: &str) -> Result<Vec<DirEntry>> {
76 self.check_destroyed()?;
77
78 let full_path = fs::validate_path(&self.config.work_dir, path)?;
79 let mut entries = Vec::new();
80
81 let mut rd = tokio::fs::read_dir(&full_path).await?;
82 while let Some(entry) = rd.next_entry().await? {
83 let metadata = entry.metadata().await?;
84 entries.push(DirEntry {
85 name: entry.file_name().to_string_lossy().to_string(),
86 is_dir: metadata.is_dir(),
87 is_file: metadata.is_file(),
88 size: metadata.len(),
89 });
90 }
91
92 entries.sort_by(|a, b| a.name.cmp(&b.name));
93 Ok(entries)
94 }
95
96 pub async fn diff(&self) -> Result<Vec<FsChange>> {
98 self.check_destroyed()?;
99
100 let overlay = self.overlay.lock().await;
101 match overlay.as_ref() {
102 Some(o) => o.diff(),
103 None => Err(SandboxError::Destroyed),
104 }
105 }
106
107 pub async fn destroy(&self) -> Result<()> {
109 self.destroyed
110 .store(true, std::sync::atomic::Ordering::SeqCst);
111 let mut overlay = self.overlay.lock().await;
112 *overlay = None;
113 Ok(())
114 }
115
116 fn check_destroyed(&self) -> Result<()> {
117 if self.destroyed.load(std::sync::atomic::Ordering::SeqCst) {
118 Err(SandboxError::Destroyed)
119 } else {
120 Ok(())
121 }
122 }
123}
124
125#[derive(Debug, Clone)]
127pub struct DirEntry {
128 pub name: String,
129 pub is_dir: bool,
130 pub is_file: bool,
131 pub size: u64,
132}