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 exec_js(&self, code: &str) -> Result<ExecResult> {
52 self.exec("node", &["-e".to_string(), code.to_string()])
53 .await
54 }
55
56 pub async fn read_file(&self, path: &str) -> Result<Vec<u8>> {
58 self.check_destroyed()?;
59
60 let full_path = fs::validate_path(&self.config.work_dir, path)?;
61 let content = tokio::fs::read(&full_path).await?;
62 Ok(content)
63 }
64
65 pub async fn write_file(&self, path: &str, contents: &[u8]) -> Result<()> {
67 self.check_destroyed()?;
68
69 let full_path = fs::validate_path(&self.config.work_dir, path)?;
70
71 if let Some(parent) = full_path.parent() {
73 tokio::fs::create_dir_all(parent).await?;
74 }
75
76 tokio::fs::write(&full_path, contents).await?;
77 Ok(())
78 }
79
80 pub async fn list_dir(&self, path: &str) -> Result<Vec<DirEntry>> {
82 self.check_destroyed()?;
83
84 let full_path = fs::validate_path(&self.config.work_dir, path)?;
85 let mut entries = Vec::new();
86
87 let mut rd = tokio::fs::read_dir(&full_path).await?;
88 while let Some(entry) = rd.next_entry().await? {
89 let metadata = entry.metadata().await?;
90 entries.push(DirEntry {
91 name: entry.file_name().to_string_lossy().to_string(),
92 is_dir: metadata.is_dir(),
93 is_file: metadata.is_file(),
94 size: metadata.len(),
95 });
96 }
97
98 entries.sort_by(|a, b| a.name.cmp(&b.name));
99 Ok(entries)
100 }
101
102 pub async fn diff(&self) -> Result<Vec<FsChange>> {
104 self.check_destroyed()?;
105
106 let overlay = self.overlay.lock().await;
107 match overlay.as_ref() {
108 Some(o) => o.diff(),
109 None => Err(SandboxError::Destroyed),
110 }
111 }
112
113 pub async fn destroy(&self) -> Result<()> {
115 self.destroyed
116 .store(true, std::sync::atomic::Ordering::SeqCst);
117 let mut overlay = self.overlay.lock().await;
118 *overlay = None;
119 Ok(())
120 }
121
122 fn check_destroyed(&self) -> Result<()> {
123 if self.destroyed.load(std::sync::atomic::Ordering::SeqCst) {
124 Err(SandboxError::Destroyed)
125 } else {
126 Ok(())
127 }
128 }
129}
130
131#[derive(Debug, Clone)]
133pub struct DirEntry {
134 pub name: String,
135 pub is_dir: bool,
136 pub is_file: bool,
137 pub size: u64,
138}