evalbox_sandbox/notify/
virtual_fs.rs1use std::path::{Path, PathBuf};
16
17#[derive(Debug, Clone)]
22pub struct VirtualFs {
23 mappings: Vec<(PathBuf, PathBuf)>,
25}
26
27impl VirtualFs {
28 pub fn new(workspace_root: &Path) -> Self {
30 let mappings = vec![
31 (PathBuf::from("/work"), workspace_root.join("work")),
32 (PathBuf::from("/tmp"), workspace_root.join("tmp")),
33 (PathBuf::from("/home"), workspace_root.join("home")),
34 ];
35 Self { mappings }
36 }
37
38 pub fn empty() -> Self {
40 Self {
41 mappings: Vec::new(),
42 }
43 }
44
45 pub fn add_mapping(&mut self, virtual_path: impl Into<PathBuf>, real_path: impl Into<PathBuf>) {
47 self.mappings.push((virtual_path.into(), real_path.into()));
48 }
49
50 pub fn translate(&self, path: &str) -> Option<PathBuf> {
55 let path = Path::new(path);
56 for (virtual_prefix, real_prefix) in &self.mappings {
57 if let Ok(suffix) = path.strip_prefix(virtual_prefix) {
58 return Some(real_prefix.join(suffix));
59 }
60 }
61 None
62 }
63
64 pub fn is_allowed(&self, path: &str) -> bool {
68 let path = Path::new(path);
69
70 for (virtual_prefix, _) in &self.mappings {
72 if path.starts_with(virtual_prefix) {
73 return true;
74 }
75 }
76
77 let system_prefixes = ["/usr", "/bin", "/lib", "/lib64", "/etc", "/proc", "/dev"];
79 for prefix in &system_prefixes {
80 if path.starts_with(prefix) {
81 return true;
82 }
83 }
84
85 false
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn default_mappings() {
95 let vfs = VirtualFs::new(Path::new("/tmp/evalbox-abc123"));
96
97 assert_eq!(
98 vfs.translate("/work/main.py"),
99 Some(PathBuf::from("/tmp/evalbox-abc123/work/main.py"))
100 );
101 assert_eq!(
102 vfs.translate("/tmp/output.txt"),
103 Some(PathBuf::from("/tmp/evalbox-abc123/tmp/output.txt"))
104 );
105 assert_eq!(
106 vfs.translate("/home/.bashrc"),
107 Some(PathBuf::from("/tmp/evalbox-abc123/home/.bashrc"))
108 );
109 }
110
111 #[test]
112 fn no_translation_for_system_paths() {
113 let vfs = VirtualFs::new(Path::new("/tmp/evalbox-abc123"));
114 assert_eq!(vfs.translate("/usr/bin/python3"), None);
115 assert_eq!(vfs.translate("/etc/passwd"), None);
116 }
117
118 #[test]
119 fn is_allowed_checks() {
120 let vfs = VirtualFs::new(Path::new("/tmp/evalbox-abc123"));
121
122 assert!(vfs.is_allowed("/work/test.py"));
123 assert!(vfs.is_allowed("/tmp/output"));
124 assert!(vfs.is_allowed("/usr/bin/python3"));
125 assert!(vfs.is_allowed("/etc/passwd"));
126 assert!(vfs.is_allowed("/proc/self/status"));
127 assert!(!vfs.is_allowed("/root/.ssh/id_rsa"));
128 assert!(!vfs.is_allowed("/var/log/syslog"));
129 }
130
131 #[test]
132 fn custom_mapping() {
133 let mut vfs = VirtualFs::empty();
134 vfs.add_mapping("/data", "/mnt/shared/data");
135
136 assert_eq!(
137 vfs.translate("/data/file.csv"),
138 Some(PathBuf::from("/mnt/shared/data/file.csv"))
139 );
140 assert_eq!(vfs.translate("/work/test"), None);
141 }
142}