1use std::ffi::{OsStr, OsString};
39use std::time::Instant;
40
41use async_trait::async_trait;
42use tokio::io::AsyncWriteExt;
43use tokio::process::Command;
44use tracing::{Span, instrument};
45
46use crate::backend::{BackendCapabilities, EnforcedLimits, SandboxBackend};
47use crate::error::SandboxError;
48use crate::sandbox::{SandboxEnforcer, SandboxPolicy};
49use crate::types::{ExecRequest, ExecResult, Language};
50
51const MAX_OUTPUT_BYTES: usize = 1_024 * 1_024;
53
54const NON_WINDOWS_TOOLCHAIN_ENV_KEYS: &[&str] = &[
56 "PATH",
57 "DEVELOPER_DIR",
58 "SDKROOT",
59 "HOME",
60 "TMPDIR",
61 "RUSTUP_HOME",
62 "CARGO_HOME",
63 "RUSTUP_TOOLCHAIN",
64];
65
66const WINDOWS_TOOLCHAIN_ENV_KEYS: &[&str] = &[
72 "PATH",
73 "LIB",
74 "LIBPATH",
75 "INCLUDE",
76 "SystemRoot",
77 "TEMP",
78 "TMP",
79 "USERPROFILE",
80 "RUSTUP_HOME",
81 "RUSTUP_TOOLCHAIN",
82];
83
84#[derive(Debug, Clone)]
100pub struct ProcessConfig {
101 pub rustc_path: String,
103 pub python_path: String,
105 pub node_path: String,
107 pub max_output_bytes: usize,
113}
114
115impl Default for ProcessConfig {
116 fn default() -> Self {
117 Self {
118 rustc_path: "rustc".to_string(),
119 python_path: "python3".to_string(),
120 node_path: "node".to_string(),
121 max_output_bytes: MAX_OUTPUT_BYTES,
122 }
123 }
124}
125
126pub struct ProcessBackend {
161 config: ProcessConfig,
162 enforcer: Option<Box<dyn SandboxEnforcer>>,
163 policy: Option<SandboxPolicy>,
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub enum IsolationClass {
172 SubprocessOnly,
177 OsEnforced,
180}
181
182fn resolve_program(program: &OsStr) -> Option<std::path::PathBuf> {
188 let as_path = std::path::Path::new(program);
189 if as_path.components().count() > 1 {
190 return None;
191 }
192
193 let path_var = std::env::var_os("PATH")?;
194 std::env::split_paths(&path_var).find_map(|dir| {
195 let candidate = dir.join(program);
196 candidate.is_file().then_some(candidate)
197 })
198}
199
200impl ProcessBackend {
201 pub fn new(config: ProcessConfig) -> Self {
206 Self { config, enforcer: None, policy: None }
207 }
208
209 pub fn isolation(&self) -> IsolationClass {
214 match (self.enforcer.is_some(), self.policy.is_some()) {
215 (true, true) => IsolationClass::OsEnforced,
216 _ => IsolationClass::SubprocessOnly,
217 }
218 }
219
220 pub fn with_sandbox(
229 config: ProcessConfig,
230 enforcer: Box<dyn SandboxEnforcer>,
231 policy: SandboxPolicy,
232 ) -> Self {
233 Self { config, enforcer: Some(enforcer), policy: Some(policy) }
234 }
235}
236
237impl Default for ProcessBackend {
238 fn default() -> Self {
239 Self::new(ProcessConfig::default())
240 }
241}
242
243impl std::fmt::Debug for ProcessBackend {
245 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246 f.debug_struct("ProcessBackend")
247 .field("config", &self.config)
248 .field("enforcer", &self.enforcer.as_ref().map(|e| e.name()))
249 .field("policy", &self.policy)
250 .finish()
251 }
252}
253
254fn truncate_utf8(bytes: Vec<u8>, max_bytes: usize) -> String {
257 if bytes.len() <= max_bytes {
258 return String::from_utf8_lossy(&bytes).into_owned();
259 }
260 let truncated = &bytes[..max_bytes];
261 let mut end = max_bytes;
263 while end > 0 && std::str::from_utf8(&truncated[..end]).is_err() {
264 end -= 1;
265 }
266 std::str::from_utf8(&bytes[..end]).unwrap_or("").to_string()
267}
268
269fn note_truncation(mut text: String, discarded: bool) -> String {
275 if discarded {
276 text.push_str("\n... (truncated: output exceeded the configured limit)");
277 }
278 text
279}
280
281async fn read_capped<R>(mut reader: R, cap: usize) -> std::io::Result<(Vec<u8>, bool)>
289where
290 R: tokio::io::AsyncRead + Unpin,
291{
292 use tokio::io::AsyncReadExt;
293
294 let mut retained = Vec::new();
295 let mut chunk = [0u8; 8192];
296 let mut discarded = false;
297
298 loop {
299 let read = reader.read(&mut chunk).await?;
300 if read == 0 {
301 break;
302 }
303 let room = cap.saturating_sub(retained.len());
304 if room == 0 {
305 discarded = true;
306 continue;
307 }
308 let take = room.min(read);
309 retained.extend_from_slice(&chunk[..take]);
310 if take < read {
311 discarded = true;
312 }
313 }
314
315 Ok((retained, discarded))
316}
317
318#[async_trait]
319impl SandboxBackend for ProcessBackend {
320 fn name(&self) -> &str {
321 "process"
322 }
323
324 fn capabilities(&self) -> BackendCapabilities {
325 let has_enforcer = self.enforcer.is_some();
326 let denies_network = self.policy.as_ref().is_some_and(|p| !p.allow_network);
327
328 BackendCapabilities {
329 supported_languages: vec![
330 Language::Rust,
331 Language::Python,
332 Language::JavaScript,
333 Language::TypeScript,
334 Language::Command,
335 ],
336 isolation_class: if has_enforcer {
337 "process+sandbox".to_string()
338 } else {
339 "process".to_string()
340 },
341 enforced_limits: EnforcedLimits {
342 timeout: true,
343 memory: false,
344 network_isolation: has_enforcer && denies_network,
345 filesystem_write_isolation: has_enforcer,
346 filesystem_read_isolation: has_enforcer && cfg!(target_os = "linux"),
350 environment_isolation: true,
351 },
352 }
353 }
354
355 #[instrument(
356 skip_all,
357 fields(
358 backend = "process",
359 language = %request.language,
360 exit_code,
361 duration_ms,
362 )
363 )]
364 async fn execute(&self, request: ExecRequest) -> Result<ExecResult, SandboxError> {
365 if let Some(limit) = request.memory_limit_mb {
366 tracing::debug!(
367 memory_limit_mb = limit,
368 "memory limit not enforced by process backend"
369 );
370 }
371
372 match request.language {
373 Language::Rust => self.execute_rust(&request).await,
374 Language::Python => self.execute_python(&request).await,
375 Language::JavaScript | Language::TypeScript => self.execute_javascript(&request).await,
376 Language::Command => self.execute_command(&request).await,
377 Language::Wasm => Err(SandboxError::InvalidRequest(
378 "Wasm execution is not supported by ProcessBackend. Use WasmBackend instead."
379 .to_string(),
380 )),
381 }
382 }
383}
384
385impl ProcessBackend {
386 async fn execute_rust(&self, request: &ExecRequest) -> Result<ExecResult, SandboxError> {
388 let dir = tempfile::tempdir()?;
389 let src_path = dir.path().join("main.rs");
390 let bin_path = dir.path().join("main");
391
392 std::fs::write(&src_path, &request.code)?;
393
394 let toolchain_env = Self::toolchain_env();
400 #[cfg(windows)]
401 let has_msvc_library_path =
402 toolchain_env.iter().any(|(key, _)| key.eq_ignore_ascii_case("LIB"));
403
404 let compile_result = {
405 let mut cmd = Command::new(&self.config.rustc_path);
406 #[cfg(windows)]
411 cmd.arg("-Clinker=rust-lld");
412 cmd.arg(&src_path).arg("-o").arg(&bin_path);
413 self.run_command_with_env(cmd, request, &toolchain_env).await?
414 };
415
416 #[cfg(windows)]
417 let compile_result = {
418 let mut result = compile_result;
419 if result.exit_code != 0 && !has_msvc_library_path {
420 result.stderr.push_str(
421 "\nWindows Rust linking requires the MSVC Build Tools and Windows SDK. \
422 Install the `Desktop development with C++` workload; ProcessBackend could \
423 not discover its LIB paths from this host.",
424 );
425 }
426 result
427 };
428
429 if compile_result.exit_code != 0 {
430 Span::current().record("exit_code", compile_result.exit_code);
431 Span::current().record("duration_ms", compile_result.duration.as_millis() as u64);
432 return Ok(compile_result);
433 }
434
435 self.run_binary(&bin_path, request).await
437 }
438
439 async fn execute_python(&self, request: &ExecRequest) -> Result<ExecResult, SandboxError> {
441 let dir = tempfile::tempdir()?;
442 let src_path = dir.path().join("script.py");
443 std::fs::write(&src_path, &request.code)?;
444
445 let mut cmd = Command::new(&self.config.python_path);
446 cmd.arg(&src_path);
447 self.run_command(cmd, request).await
448 }
449
450 async fn execute_javascript(&self, request: &ExecRequest) -> Result<ExecResult, SandboxError> {
452 let dir = tempfile::tempdir()?;
453 let src_path = dir.path().join("script.js");
454 std::fs::write(&src_path, &request.code)?;
455
456 let mut cmd = Command::new(&self.config.node_path);
457 cmd.arg(&src_path);
458 self.run_command(cmd, request).await
459 }
460
461 async fn execute_command(&self, request: &ExecRequest) -> Result<ExecResult, SandboxError> {
463 #[cfg(windows)]
464 let cmd = {
465 use std::os::windows::process::CommandExt;
466
467 let mut c = Command::new("cmd");
468 c.arg("/D").arg("/C");
469 c.as_std_mut().raw_arg(&request.code);
474 c
475 };
476 #[cfg(not(windows))]
477 let cmd = {
478 let mut c = Command::new("sh");
479 c.arg("-c").arg(&request.code);
480 c
481 };
482 self.run_command(cmd, request).await
483 }
484
485 async fn run_binary(
487 &self,
488 bin_path: &std::path::Path,
489 request: &ExecRequest,
490 ) -> Result<ExecResult, SandboxError> {
491 let cmd = Command::new(bin_path);
492 self.run_command(cmd, request).await
493 }
494
495 async fn run_command(
500 &self,
501 cmd: Command,
502 request: &ExecRequest,
503 ) -> Result<ExecResult, SandboxError> {
504 self.run_command_with_env(cmd, request, &[]).await
505 }
506
507 fn toolchain_env() -> Vec<(String, OsString)> {
518 let keys =
525 if cfg!(windows) { WINDOWS_TOOLCHAIN_ENV_KEYS } else { NON_WINDOWS_TOOLCHAIN_ENV_KEYS };
526
527 let environment: Vec<(String, OsString)> = keys
528 .iter()
529 .filter_map(|key| std::env::var_os(key).map(|value| ((*key).to_string(), value)))
530 .collect();
531
532 #[cfg(windows)]
533 let mut environment = environment;
534
535 #[cfg(windows)]
536 if !environment.iter().any(|(key, _)| key.eq_ignore_ascii_case("LIB"))
537 && let Some(linker) = find_msvc_tools::find(std::env::consts::ARCH, "link.exe")
538 {
539 for (key, value) in linker.get_envs() {
540 let Some(value) = value else {
541 continue;
542 };
543 let Some(allowed_key) = WINDOWS_TOOLCHAIN_ENV_KEYS
544 .iter()
545 .find(|allowed| key.eq_ignore_ascii_case(OsStr::new(allowed)))
546 else {
547 continue;
548 };
549 if !environment
550 .iter()
551 .any(|(existing, _)| existing.eq_ignore_ascii_case(allowed_key))
552 {
553 environment.push(((*allowed_key).to_string(), value.to_os_string()));
554 }
555 }
556 }
557
558 environment
559 }
560
561 async fn run_command_with_env(
563 &self,
564 cmd: Command,
565 request: &ExecRequest,
566 extra_env: &[(String, OsString)],
567 ) -> Result<ExecResult, SandboxError> {
568 let mut cmd = if let (Some(enforcer), Some(policy)) = (&self.enforcer, &self.policy) {
572 let std_cmd = cmd.as_std();
573 let program = std_cmd.get_program();
574 let args: Vec<OsString> = std_cmd.get_args().map(OsStr::to_owned).collect();
575
576 let wrapped = enforcer.wrap_command(program, &args, policy)?;
577
578 let mut new_cmd = Command::new(&wrapped.program);
579 new_cmd.args(&wrapped.args);
580
581 enforcer.configure_command(&mut new_cmd, policy)?;
583
584 new_cmd
585 } else {
586 cmd
587 };
588
589 {
594 let program = cmd.as_std().get_program().to_owned();
595 if let Some(resolved) = resolve_program(&program) {
596 let args: Vec<OsString> = cmd.as_std().get_args().map(OsStr::to_owned).collect();
597 let mut resolved_cmd = Command::new(resolved);
598 resolved_cmd.args(&args);
599 cmd = resolved_cmd;
600 }
601 }
602
603 cmd.env_clear();
607 for (k, v) in extra_env {
608 cmd.env(k, v);
609 }
610 if let Some(policy) = &self.policy {
611 for (k, v) in &policy.env {
612 cmd.env(k, v);
613 }
614 }
615 for (k, v) in &request.env {
616 cmd.env(k, v);
617 }
618 cmd.kill_on_drop(true);
619
620 #[cfg(unix)]
626 {
627 use std::os::unix::process::CommandExt;
628 cmd.as_std_mut().process_group(0);
629 }
630
631 cmd.stdout(std::process::Stdio::piped());
632 cmd.stderr(std::process::Stdio::piped());
633
634 if request.stdin.is_some() {
635 cmd.stdin(std::process::Stdio::piped());
636 } else {
637 cmd.stdin(std::process::Stdio::null());
638 }
639
640 let start = Instant::now();
641 let mut child = cmd.spawn()?;
642 #[cfg(unix)]
643 let process_group = child.id().map(|id| id as i32);
644
645 if let Some(ref input) = request.stdin
647 && let Some(mut stdin_handle) = child.stdin.take()
648 {
649 stdin_handle.write_all(input.as_bytes()).await?;
650 drop(stdin_handle);
651 }
652
653 let cap = self.config.max_output_bytes;
657 let stdout_pipe = child.stdout.take();
658 let stderr_pipe = child.stderr.take();
659 let stdout_reader = tokio::spawn(async move {
660 match stdout_pipe {
661 Some(pipe) => read_capped(pipe, cap).await,
662 None => Ok((Vec::new(), false)),
663 }
664 });
665 let stderr_reader = tokio::spawn(async move {
666 match stderr_pipe {
667 Some(pipe) => read_capped(pipe, cap).await,
668 None => Ok((Vec::new(), false)),
669 }
670 });
671
672 let output = tokio::time::timeout(request.timeout, async {
673 let status = child.wait().await?;
674 let (stdout, stdout_discarded) =
675 stdout_reader.await.map_err(std::io::Error::other)??;
676 let (stderr, stderr_discarded) =
677 stderr_reader.await.map_err(std::io::Error::other)??;
678 Ok::<_, std::io::Error>((status, stdout, stdout_discarded, stderr, stderr_discarded))
679 })
680 .await;
681 let duration = start.elapsed();
682
683 match output {
684 Ok(Ok((status, stdout_bytes, stdout_discarded, stderr_bytes, stderr_discarded))) => {
685 let exit_code = status.code().unwrap_or(-1);
686 if stdout_discarded || stderr_discarded {
687 tracing::warn!(
688 max_output_bytes = cap,
689 stdout.truncated = stdout_discarded,
690 stderr.truncated = stderr_discarded,
691 "sandbox output exceeded the cap and was truncated"
692 );
693 }
694 let cap = self.config.max_output_bytes;
695 let stdout = note_truncation(truncate_utf8(stdout_bytes, cap), stdout_discarded);
696 let stderr = note_truncation(truncate_utf8(stderr_bytes, cap), stderr_discarded);
697
698 Span::current().record("exit_code", exit_code);
699 Span::current().record("duration_ms", duration.as_millis() as u64);
700
701 Ok(ExecResult { stdout, stderr, exit_code, duration })
702 }
703 Ok(Err(e)) => {
704 Err(SandboxError::ExecutionFailed(format!("failed to wait for child process: {e}")))
705 }
706 Err(_) => {
707 #[cfg(unix)]
711 if let Some(group) = process_group {
712 unsafe {
716 libc::kill(-group, libc::SIGKILL);
717 }
718 }
719 Span::current().record("duration_ms", duration.as_millis() as u64);
720 Err(SandboxError::Timeout { timeout: request.timeout })
721 }
722 }
723 }
724}
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729 use std::collections::HashMap;
730 use std::time::Duration;
731
732 fn make_request(language: Language, code: &str) -> ExecRequest {
733 let mut env = HashMap::new();
734 if let Ok(path) = std::env::var("PATH") {
737 env.insert("PATH".to_string(), path);
738 }
739 if let Ok(sr) = std::env::var("SYSTEMROOT") {
741 env.insert("SYSTEMROOT".to_string(), sr);
742 }
743 ExecRequest {
744 language,
745 code: code.to_string(),
746 stdin: None,
747 timeout: Duration::from_secs(30),
748 memory_limit_mb: None,
749 env,
750 }
751 }
752
753 #[tokio::test]
754 async fn test_python_execution() {
755 let backend = ProcessBackend::default();
756 let request = make_request(Language::Python, "print('hello')");
757 let result = backend.execute(request).await.unwrap();
758 assert!(result.stdout.contains("hello"), "stdout: {}", result.stdout);
759 assert_eq!(result.exit_code, 0);
760 }
761
762 #[tokio::test]
763 async fn test_javascript_execution() {
764 if std::process::Command::new("node").arg("--version").output().is_err() {
766 eprintln!("skipping test_javascript_execution: node not found");
767 return;
768 }
769 let backend = ProcessBackend::default();
770 let request = make_request(Language::JavaScript, "console.log('hello')");
771 let result = backend.execute(request).await.unwrap();
772 assert!(result.stdout.contains("hello"), "stdout: {}", result.stdout);
773 assert_eq!(result.exit_code, 0);
774 }
775
776 #[tokio::test]
777 async fn test_command_execution() {
778 let backend = ProcessBackend::default();
779 let request = make_request(Language::Command, "echo hello");
780 let result = backend.execute(request).await.unwrap();
781 assert!(result.stdout.contains("hello"), "stdout: {}", result.stdout);
782 assert_eq!(result.exit_code, 0);
783 }
784
785 #[tokio::test]
786 #[cfg(windows)]
787 async fn test_command_supports_quoted_script_paths() {
788 let directory = tempfile::tempdir().unwrap();
789 let script = directory.path().join("quoted helper.cmd");
790 std::fs::write(&script, "@echo quoted-path-ok\r\n").unwrap();
791
792 let backend = ProcessBackend::default();
793 let request = make_request(Language::Command, &format!("\"{}\"", script.display()));
794 let result = backend.execute(request).await.unwrap();
795
796 assert_eq!(result.exit_code, 0, "stderr: {}", result.stderr);
797 assert!(result.stdout.contains("quoted-path-ok"), "stdout: {}", result.stdout);
798 }
799
800 #[tokio::test]
801 async fn test_timeout_enforcement() {
802 let backend = ProcessBackend::default();
803 let code =
804 if cfg!(windows) { "ping -n 11 127.0.0.1".to_string() } else { "sleep 10".to_string() };
805 let mut request = make_request(Language::Command, &code);
806 request.timeout = Duration::from_secs(1);
807 let result = backend.execute(request).await;
808 assert!(
809 matches!(result, Err(SandboxError::Timeout { .. })),
810 "expected Timeout, got: {result:?}"
811 );
812 }
813
814 #[tokio::test]
815 #[cfg(unix)]
816 async fn test_timeout_terminates_background_descendants() {
817 let backend = ProcessBackend::default();
818 let directory = tempfile::tempdir().unwrap();
819 let marker = directory.path().join("escaped-child");
820 let escaped_marker = marker.to_string_lossy().replace('\'', "'\\''");
821 let code = format!("(sleep 1; touch '{escaped_marker}') & wait");
822 let mut request = make_request(Language::Command, &code);
823 request.timeout = Duration::from_millis(100);
824
825 let result = backend.execute(request).await;
826 assert!(matches!(result, Err(SandboxError::Timeout { .. })));
827 tokio::time::sleep(Duration::from_millis(1_200)).await;
828 assert!(!marker.exists(), "a background descendant survived the execution timeout");
829 }
830
831 #[tokio::test]
832 #[cfg(not(windows))]
833 async fn test_environment_isolation() {
834 let backend = ProcessBackend::default();
835 let mut env = HashMap::new();
836 env.insert("MY_TEST_VAR".to_string(), "test_value".to_string());
837 let request = ExecRequest {
838 language: Language::Command,
839 code: "/usr/bin/env".to_string(),
841 stdin: None,
842 timeout: Duration::from_secs(10),
843 memory_limit_mb: None,
844 env,
845 };
846 let result = backend.execute(request).await.unwrap();
847 assert!(result.stdout.contains("MY_TEST_VAR=test_value"), "stdout: {}", result.stdout);
849 assert!(
851 !result.stdout.contains("HOME="),
852 "HOME should not be inherited: {}",
853 result.stdout
854 );
855 }
856
857 #[tokio::test]
858 #[cfg(windows)]
859 async fn test_environment_isolation() {
860 let backend = ProcessBackend::default();
861 let mut env = HashMap::new();
862 env.insert("MY_TEST_VAR".to_string(), "test_value".to_string());
863 let request = ExecRequest {
864 language: Language::Command,
865 code: "set MY_TEST_VAR".to_string(),
866 stdin: None,
867 timeout: Duration::from_secs(10),
868 memory_limit_mb: None,
869 env,
870 };
871 let result = backend.execute(request).await.unwrap();
872 assert!(result.stdout.contains("MY_TEST_VAR=test_value"), "stdout: {}", result.stdout);
873 }
874
875 #[tokio::test]
876 async fn test_nonzero_exit_code() {
877 let backend = ProcessBackend::default();
878 let code = if cfg!(windows) { "exit /b 42" } else { "exit 42" };
879 let request = make_request(Language::Command, code);
880 let result = backend.execute(request).await.unwrap();
881 assert_eq!(result.exit_code, 42);
882 }
883
884 #[tokio::test]
885 async fn test_wasm_returns_invalid_request() {
886 let backend = ProcessBackend::default();
887 let request = make_request(Language::Wasm, "");
888 let result = backend.execute(request).await;
889 assert!(
890 matches!(result, Err(SandboxError::InvalidRequest(_))),
891 "expected InvalidRequest, got: {result:?}"
892 );
893 }
894
895 #[tokio::test]
902 async fn read_capped_retains_at_most_the_cap() {
903 let cap = 4_096;
904 let source = vec![b'x'; cap * 256];
906
907 let (retained, discarded) = read_capped(&source[..], cap).await.expect("reads");
908
909 assert_eq!(retained.len(), cap, "retained buffer must stop at the cap");
910 assert!(discarded, "the overflow must be reported as discarded");
911 }
912
913 #[tokio::test]
915 async fn read_capped_retains_everything_under_the_cap() {
916 let source = vec![b'y'; 100];
917
918 let (retained, discarded) = read_capped(&source[..], 4_096).await.expect("reads");
919
920 assert_eq!(retained, source);
921 assert!(!discarded);
922 }
923
924 #[tokio::test]
926 async fn read_capped_handles_the_exact_boundary() {
927 let cap = 8_192;
928 let source = vec![b'z'; cap];
929
930 let (retained, discarded) = read_capped(&source[..], cap).await.expect("reads");
931
932 assert_eq!(retained.len(), cap);
933 assert!(!discarded, "reaching the cap exactly discards nothing");
934 }
935
936 #[test]
937 fn test_truncate_utf8_within_limit() {
938 let data = "hello world".as_bytes().to_vec();
939 let result = truncate_utf8(data, 1024);
940 assert_eq!(result, "hello world");
941 }
942
943 #[test]
944 fn test_truncate_utf8_at_boundary() {
945 let data = "café".as_bytes().to_vec(); let result = truncate_utf8(data, 4);
949 assert_eq!(result, "caf");
950 }
951
952 #[test]
953 fn test_capabilities() {
954 let backend = ProcessBackend::default();
955 let caps = backend.capabilities();
956 assert_eq!(caps.isolation_class, "process");
957 assert!(caps.enforced_limits.timeout);
958 assert!(caps.enforced_limits.environment_isolation);
959 assert!(!caps.enforced_limits.memory);
960 assert!(!caps.enforced_limits.network_isolation);
961 assert!(!caps.enforced_limits.filesystem_write_isolation);
962 assert!(!caps.enforced_limits.filesystem_read_isolation);
963 assert!(caps.supported_languages.contains(&Language::Rust));
964 assert!(caps.supported_languages.contains(&Language::Python));
965 assert!(caps.supported_languages.contains(&Language::JavaScript));
966 assert!(caps.supported_languages.contains(&Language::TypeScript));
967 assert!(caps.supported_languages.contains(&Language::Command));
968 assert!(!caps.supported_languages.contains(&Language::Wasm));
969 }
970
971 #[test]
972 fn test_name() {
973 let backend = ProcessBackend::default();
974 assert_eq!(backend.name(), "process");
975 }
976
977 #[test]
978 fn test_process_config_default() {
979 let config = ProcessConfig::default();
980 assert_eq!(config.rustc_path, "rustc");
981 assert_eq!(config.python_path, "python3");
982 assert_eq!(config.node_path, "node");
983 }
984
985 #[test]
986 fn windows_compiler_environment_is_a_minimal_allowlist() {
987 assert_eq!(
988 WINDOWS_TOOLCHAIN_ENV_KEYS,
989 &[
990 "PATH",
991 "LIB",
992 "LIBPATH",
993 "INCLUDE",
994 "SystemRoot",
995 "TEMP",
996 "TMP",
997 "USERPROFILE",
998 "RUSTUP_HOME",
999 "RUSTUP_TOOLCHAIN",
1000 ]
1001 );
1002 }
1003}