1#![forbid(unsafe_code)]
19
20pub(crate) mod ast;
21pub(crate) mod commands;
22pub(crate) mod error;
23pub(crate) mod fs;
24pub(crate) mod interpreter;
25pub(crate) mod lexer;
26pub(crate) mod parser;
27
28use std::collections::HashMap;
29use std::sync::Arc;
30use std::sync::atomic::AtomicBool;
31
32pub use error::{Error, ExecError, FsError, LimitKind, ParseError};
33pub use commands::{CommandFn, CommandContext};
34pub use fs::VirtualFs;
35pub use fs::memory::InMemoryFs;
36pub use fs::mountable::MountableFs;
37pub use fs::overlay::OverlayFs;
38pub use fs::readwrite::ReadWriteFs;
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct ExecResult {
43 pub stdout: String,
44 pub stderr: String,
45 pub exit_code: i32,
46 pub env: HashMap<String, String>,
47}
48
49#[derive(Default)]
51pub struct ExecOptions<'a> {
52 pub stdin: Option<&'a str>,
54 pub env: Option<&'a HashMap<String, String>>,
56 pub cwd: Option<&'a str>,
58 pub cancel: Option<Arc<AtomicBool>>,
60}
61
62impl std::fmt::Debug for ExecOptions<'_> {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 f.debug_struct("ExecOptions")
65 .field("stdin", &self.stdin)
66 .field("env", &self.env)
67 .field("cwd", &self.cwd)
68 .field("cancel", &self.cancel.as_ref().map(|c| c.load(std::sync::atomic::Ordering::Relaxed)))
69 .finish()
70 }
71}
72
73#[cfg(feature = "network")]
78#[derive(Debug, Clone)]
79pub struct NetworkPolicy {
80 pub allowed_url_prefixes: Vec<String>,
83 pub block_private_ips: bool,
86 pub allowed_methods: Vec<String>,
88 pub max_response_size: usize,
90 pub max_redirects: u32,
92}
93
94#[cfg(feature = "network")]
95impl Default for NetworkPolicy {
96 fn default() -> Self {
97 Self {
98 allowed_url_prefixes: Vec::new(),
99 block_private_ips: true,
100 allowed_methods: vec![
101 "GET".into(),
102 "HEAD".into(),
103 "POST".into(),
104 "PUT".into(),
105 "DELETE".into(),
106 "PATCH".into(),
107 ],
108 max_response_size: 10 * 1024 * 1024, max_redirects: 20,
110 }
111 }
112}
113
114#[derive(Debug, Clone)]
116pub struct ExecutionLimits {
117 pub max_call_depth: u32,
118 pub max_command_count: u32,
119 pub max_loop_iterations: u32,
120 pub max_output_size: usize,
121 pub max_substitution_depth: u32,
122 pub max_brace_expansion: u32,
123 pub max_glob_operations: u32,
124 pub max_string_length: usize,
125 pub max_array_elements: usize,
126 pub max_source_depth: u32,
127 pub max_input_size: usize,
128}
129
130impl Default for ExecutionLimits {
131 fn default() -> Self {
132 Self {
133 max_call_depth: 100,
134 max_command_count: 10_000,
135 max_loop_iterations: 10_000,
136 max_output_size: 10 * 1024 * 1024,
137 max_substitution_depth: 50,
138 max_brace_expansion: 10_000,
139 max_glob_operations: 100_000,
140 max_string_length: 10 * 1024 * 1024,
141 max_array_elements: 100_000,
142 max_source_depth: 100,
143 max_input_size: 1_000_000,
144 }
145 }
146}
147
148#[derive(Debug, Clone)]
149pub struct SessionLimits {
150 pub max_total_commands: u64,
151 pub max_exec_calls: u64,
152}
153
154impl Default for SessionLimits {
155 fn default() -> Self {
156 Self {
157 max_total_commands: 100_000,
158 max_exec_calls: 1_000,
159 }
160 }
161}
162
163pub struct Shell {
169 fs: Box<dyn VirtualFs>,
170 default_env: HashMap<String, String>,
171 cwd: String,
172 limits: ExecutionLimits,
173 registry: commands::CommandRegistry,
174 #[cfg(feature = "network")]
175 network_policy: Option<NetworkPolicy>,
176 session_limits: Option<SessionLimits>,
177 session_command_count: u64,
178 session_exec_count: u64,
179}
180
181impl Shell {
182 pub fn new() -> Self {
184 Builder::new().build()
185 }
186
187 pub fn builder() -> Builder {
189 Builder::new()
190 }
191
192 pub fn exec(&mut self, command: &str) -> Result<ExecResult, Error> {
204 self.check_session_limits()?;
205 if command.len() > self.limits.max_input_size {
206 return Err(Error::LimitExceeded(crate::error::LimitKind::InputSize));
207 }
208 let script = parser::parse(command)?;
209 let (result, cmd_count) = interpreter::execute(
210 &script,
211 &*self.fs,
212 &self.default_env,
213 &self.cwd,
214 &self.limits,
215 &self.registry,
216 "",
217 None,
218 #[cfg(feature = "network")]
219 self.network_policy.as_ref(),
220 );
221 self.update_session_counters(cmd_count);
222 result
223 }
224
225 #[allow(clippy::needless_pass_by_value)]
227 pub fn exec_with(&mut self, command: &str, options: ExecOptions<'_>) -> Result<ExecResult, Error> {
228 self.check_session_limits()?;
229 if command.len() > self.limits.max_input_size {
230 return Err(Error::LimitExceeded(crate::error::LimitKind::InputSize));
231 }
232 let script = parser::parse(command)?;
233 let env = match options.env {
234 Some(override_env) => {
235 let mut e = self.default_env.clone();
236 e.extend(override_env.iter().map(|(k, v)| (k.clone(), v.clone())));
237 e
238 }
239 None => self.default_env.clone(),
240 };
241 let cwd = options.cwd.unwrap_or(&self.cwd);
242 let stdin = options.stdin.unwrap_or("");
243 let (result, cmd_count) = interpreter::execute(
244 &script,
245 &*self.fs,
246 &env,
247 cwd,
248 &self.limits,
249 &self.registry,
250 stdin,
251 options.cancel,
252 #[cfg(feature = "network")]
253 self.network_policy.as_ref(),
254 );
255 self.update_session_counters(cmd_count);
256 result
257 }
258
259 pub fn register_command(&mut self, name: impl Into<String>, func: CommandFn) {
261 self.registry.register(name.into(), func);
262 }
263
264 pub fn exec_with_timeout(&mut self, command: &str, timeout: std::time::Duration) -> Result<ExecResult, Error> {
267 let cancel = Arc::new(AtomicBool::new(false));
268 let cancel_clone = cancel.clone();
269 std::thread::spawn(move || {
270 std::thread::sleep(timeout);
271 cancel_clone.store(true, std::sync::atomic::Ordering::Relaxed);
272 });
273 self.exec_with(command, ExecOptions {
274 cancel: Some(cancel),
275 ..Default::default()
276 })
277 }
278
279 pub fn fs(&self) -> &dyn VirtualFs {
281 &*self.fs
282 }
283
284 pub fn read_file(&self, path: &str) -> Result<String, Error> {
286 self.fs.read_file_string(path).map_err(Error::Fs)
287 }
288
289 pub fn write_file(&self, path: &str, content: &str) -> Result<(), Error> {
291 self.fs.write_file(path, content.as_bytes()).map_err(Error::Fs)
292 }
293
294 pub fn cwd(&self) -> &str {
296 &self.cwd
297 }
298
299 pub fn env(&self) -> &HashMap<String, String> {
301 &self.default_env
302 }
303
304 fn check_session_limits(&self) -> Result<(), Error> {
305 if let Some(ref sl) = self.session_limits {
306 if self.session_exec_count >= sl.max_exec_calls {
307 return Err(Error::LimitExceeded(crate::error::LimitKind::SessionExecCalls));
308 }
309 if self.session_command_count >= sl.max_total_commands {
310 return Err(Error::LimitExceeded(crate::error::LimitKind::SessionCommands));
311 }
312 }
313 Ok(())
314 }
315
316 fn update_session_counters(&mut self, commands_run: u32) {
317 self.session_exec_count += 1;
318 self.session_command_count += u64::from(commands_run);
319 }
320}
321
322impl Default for Shell {
323 fn default() -> Self {
324 Self::new()
325 }
326}
327
328impl std::fmt::Debug for Shell {
329 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330 f.debug_struct("Shell")
331 .field("cwd", &self.cwd)
332 .field("env_count", &self.default_env.len())
333 .finish_non_exhaustive()
334 }
335}
336
337pub struct Builder {
339 fs: Option<Box<dyn VirtualFs>>,
340 env: HashMap<String, String>,
341 files: Vec<(String, String)>,
342 cwd: String,
343 limits: ExecutionLimits,
344 custom_commands: Vec<(String, CommandFn)>,
345 #[cfg(feature = "network")]
346 network_policy: Option<NetworkPolicy>,
347 session_limits: Option<SessionLimits>,
348}
349
350impl Builder {
351 pub fn new() -> Self {
353 Self {
354 fs: None,
355 env: HashMap::new(),
356 files: Vec::new(),
357 cwd: String::from("/home/user"),
358 limits: ExecutionLimits::default(),
359 custom_commands: Vec::new(),
360 #[cfg(feature = "network")]
361 network_policy: None,
362 session_limits: None,
363 }
364 }
365
366 #[must_use]
368 pub fn fs(mut self, fs: impl VirtualFs + 'static) -> Self {
369 self.fs = Some(Box::new(fs));
370 self
371 }
372
373 #[must_use]
375 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
376 self.env.insert(key.into(), value.into());
377 self
378 }
379
380 #[must_use]
382 pub fn envs(mut self, vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>) -> Self {
383 for (k, v) in vars {
384 self.env.insert(k.into(), v.into());
385 }
386 self
387 }
388
389 #[must_use]
391 pub fn cwd(mut self, dir: impl Into<String>) -> Self {
392 self.cwd = dir.into();
393 self
394 }
395
396 #[must_use]
398 pub fn file(mut self, path: impl Into<String>, content: impl Into<String>) -> Self {
399 self.files.push((path.into(), content.into()));
400 self
401 }
402
403 #[must_use]
405 pub fn limits(mut self, limits: ExecutionLimits) -> Self {
406 self.limits = limits;
407 self
408 }
409
410 #[must_use]
411 pub fn session_limits(mut self, limits: SessionLimits) -> Self {
412 self.session_limits = Some(limits);
413 self
414 }
415
416 #[must_use]
418 pub fn command(mut self, name: impl Into<String>, func: CommandFn) -> Self {
419 self.custom_commands.push((name.into(), func));
420 self
421 }
422
423 #[cfg(feature = "network")]
428 #[must_use]
429 pub fn network_policy(mut self, policy: NetworkPolicy) -> Self {
430 self.network_policy = Some(policy);
431 self
432 }
433
434 pub fn build(self) -> Shell {
436 let fs: Box<dyn VirtualFs> = match self.fs {
437 Some(fs) => fs,
438 None => Box::new(InMemoryFs::new()),
439 };
440
441 let _ = fs.mkdir("/bin", true);
442 let _ = fs.mkdir("/usr/bin", true);
443 let _ = fs.mkdir("/tmp", true);
444 let _ = fs.mkdir("/dev", true);
445 let _ = fs.mkdir("/home/user", true);
446 let _ = fs.mkdir("/proc", true);
447 let _ = fs.mkdir("/proc/self", true);
448 let _ = fs.write_file("/dev/null", b"");
449 let _ = fs.write_file("/proc/version", b"Linux vbash 5.15.0 x86_64\n");
450 let _ = fs.write_file("/proc/self/exe", b"/bin/bash\n");
451
452 for (path, content) in &self.files {
453 if let Some(parent_end) = path.rfind('/') {
454 if parent_end > 0 {
455 let _ = fs.mkdir(&path[..parent_end], true);
456 }
457 }
458 let _ = fs.write_file(path, content.as_bytes());
459 }
460
461 let mut env = self.env;
462 env.entry("HOME".to_string())
463 .or_insert_with(|| "/home/user".to_string());
464 env.entry("PATH".to_string())
465 .or_insert_with(|| "/usr/bin:/bin".to_string());
466 env.entry("PWD".to_string())
467 .or_insert_with(|| self.cwd.clone());
468 env.entry("USER".to_string())
469 .or_insert_with(|| "user".to_string());
470 env.entry("HOSTNAME".to_string())
471 .or_insert_with(|| "vbash".to_string());
472 env.entry("SHELL".to_string())
473 .or_insert_with(|| "/bin/bash".to_string());
474 env.entry("TERM".to_string())
475 .or_insert_with(|| "xterm-256color".to_string());
476 env.entry("IFS".to_string())
477 .or_insert_with(|| " \t\n".to_string());
478 env.entry("OLDPWD".to_string())
479 .or_insert_with(|| self.cwd.clone());
480 env.entry("OSTYPE".to_string())
481 .or_insert_with(|| "linux-gnu".to_string());
482 env.entry("MACHTYPE".to_string())
483 .or_insert_with(|| "x86_64-pc-linux-gnu".to_string());
484 env.entry("HOSTTYPE".to_string())
485 .or_insert_with(|| "x86_64".to_string());
486 env.entry("BASH_VERSION".to_string())
487 .or_insert_with(|| "5.2.0-vbash".to_string());
488
489 let mut registry = commands::CommandRegistry::new();
490 for (name, func) in self.custom_commands {
491 registry.register(name, func);
492 }
493
494 Shell {
495 fs,
496 default_env: env,
497 cwd: self.cwd,
498 limits: self.limits,
499 registry,
500 #[cfg(feature = "network")]
501 network_policy: self.network_policy,
502 session_limits: self.session_limits,
503 session_command_count: 0,
504 session_exec_count: 0,
505 }
506 }
507}
508
509impl Default for Builder {
510 fn default() -> Self {
511 Self::new()
512 }
513}