Skip to main content

sandbox_fs/
filesystem.rs

1//! Overlay filesystem support for persistent sandbox storage
2
3use sandbox_core::{Result, SandboxError};
4use std::fs;
5use std::path::{Path, PathBuf};
6
7/// Overlay filesystem configuration
8#[derive(Debug, Clone)]
9pub struct OverlayConfig {
10    pub lower: PathBuf,
11    pub upper: PathBuf,
12    pub work: PathBuf,
13    pub merged: PathBuf,
14}
15
16impl OverlayConfig {
17    pub fn new(lower: impl AsRef<Path>, upper: impl AsRef<Path>) -> Self {
18        let lower_path = lower.as_ref().to_path_buf();
19        let upper_path = upper.as_ref().to_path_buf();
20        let work_path = upper_path
21            .parent()
22            .unwrap_or_else(|| Path::new("/tmp"))
23            .join("overlayfs-work");
24        let merged_path = upper_path
25            .parent()
26            .unwrap_or_else(|| Path::new("/tmp"))
27            .join("overlayfs-merged");
28        Self {
29            lower: lower_path,
30            upper: upper_path,
31            work: work_path,
32            merged: merged_path,
33        }
34    }
35
36    pub fn validate(&self) -> Result<()> {
37        if !self.lower.exists() {
38            return Err(SandboxError::Syscall(format!(
39                "Lower layer does not exist: {}",
40                self.lower.display()
41            )));
42        }
43        Ok(())
44    }
45
46    pub fn setup_directories(&self) -> Result<()> {
47        fs::create_dir_all(&self.upper)
48            .map_err(|e| SandboxError::Syscall(format!("Failed to create upper layer: {}", e)))?;
49        if self.work.exists() {
50            fs::remove_dir_all(&self.work).map_err(|e| {
51                SandboxError::Syscall(format!("Failed to clean work directory: {}", e))
52            })?;
53        }
54        fs::create_dir_all(&self.work).map_err(|e| {
55            SandboxError::Syscall(format!("Failed to create work directory: {}", e))
56        })?;
57        fs::create_dir_all(&self.merged).map_err(|e| {
58            SandboxError::Syscall(format!("Failed to create merged directory: {}", e))
59        })?;
60        Ok(())
61    }
62
63    pub fn get_mount_options(&self) -> Result<String> {
64        let lower_str = self
65            .lower
66            .to_str()
67            .ok_or_else(|| SandboxError::Syscall("Lower path is not valid UTF-8".to_string()))?;
68        let upper_str = self
69            .upper
70            .to_str()
71            .ok_or_else(|| SandboxError::Syscall("Upper path is not valid UTF-8".to_string()))?;
72        let work_str = self
73            .work
74            .to_str()
75            .ok_or_else(|| SandboxError::Syscall("Work path is not valid UTF-8".to_string()))?;
76        Ok(format!(
77            "lowerdir={},upperdir={},workdir={}",
78            lower_str, upper_str, work_str
79        ))
80    }
81}
82
83/// Overlay filesystem manager
84pub struct OverlayFS {
85    config: OverlayConfig,
86    mounted: bool,
87}
88
89impl OverlayFS {
90    pub fn new(config: OverlayConfig) -> Self {
91        Self {
92            config,
93            mounted: false,
94        }
95    }
96
97    pub fn setup(&mut self) -> Result<()> {
98        self.config.validate()?;
99        self.config.setup_directories()?;
100        use std::ffi::CString;
101        let fstype = CString::new("overlay")
102            .map_err(|_| SandboxError::Syscall("Invalid filesystem type".to_string()))?;
103        let source = CString::new("overlay")
104            .map_err(|_| SandboxError::Syscall("Invalid source".to_string()))?;
105        let target_str =
106            self.config.merged.to_str().ok_or_else(|| {
107                SandboxError::Syscall("Merged path is not valid UTF-8".to_string())
108            })?;
109        let target = CString::new(target_str)
110            .map_err(|_| SandboxError::Syscall("Invalid target path".to_string()))?;
111        let options_str = self.config.get_mount_options()?;
112        let options = CString::new(options_str.as_str())
113            .map_err(|_| SandboxError::Syscall("Invalid mount options".to_string()))?;
114        let ret = unsafe {
115            libc::mount(
116                source.as_ptr(),
117                target.as_ptr(),
118                fstype.as_ptr(),
119                0,
120                options.as_ptr() as *const libc::c_void,
121            )
122        };
123        if ret != 0 {
124            return Err(SandboxError::Syscall(format!(
125                "Failed to mount overlay filesystem: {}",
126                std::io::Error::last_os_error()
127            )));
128        }
129        self.mounted = true;
130        Ok(())
131    }
132
133    pub fn is_mounted(&self) -> bool {
134        self.mounted
135    }
136    pub fn merged_path(&self) -> &Path {
137        &self.config.merged
138    }
139    pub fn upper_path(&self) -> &Path {
140        &self.config.upper
141    }
142    pub fn lower_path(&self) -> &Path {
143        &self.config.lower
144    }
145
146    pub fn cleanup(&mut self) -> Result<()> {
147        if self.mounted {
148            use std::ffi::CString;
149            use std::os::unix::ffi::OsStrExt;
150            let target = CString::new(self.config.merged.as_os_str().as_bytes()).map_err(|_| {
151                SandboxError::Syscall("Invalid target path for unmount".to_string())
152            })?;
153            let ret = unsafe { libc::umount2(target.as_ptr(), libc::MNT_DETACH) };
154            if ret != 0 {
155                let err = std::io::Error::last_os_error();
156                if err.raw_os_error() != Some(libc::EINVAL)
157                    && err.raw_os_error() != Some(libc::ENOENT)
158                {
159                    return Err(SandboxError::Syscall(format!(
160                        "Failed to unmount overlay filesystem: {}",
161                        err
162                    )));
163                }
164            }
165            self.mounted = false;
166        }
167        let _ = fs::remove_dir_all(&self.config.work);
168        Ok(())
169    }
170
171    pub fn get_changes_size(&self) -> Result<u64> {
172        use walkdir::WalkDir;
173        let mut total = 0u64;
174        for entry in WalkDir::new(&self.config.upper)
175            .into_iter()
176            .filter_map(|e| e.ok())
177        {
178            if entry.file_type().is_file() {
179                total += entry
180                    .metadata()
181                    .map_err(|e| SandboxError::Syscall(e.to_string()))?
182                    .len();
183            }
184        }
185        Ok(total)
186    }
187}
188
189/// File layer information
190#[derive(Debug, Clone)]
191pub struct LayerInfo {
192    pub name: String,
193    pub size: u64,
194    pub file_count: usize,
195    pub writable: bool,
196}
197
198impl LayerInfo {
199    pub fn from_path(name: &str, path: &Path, writable: bool) -> Result<Self> {
200        use walkdir::WalkDir;
201        let mut size = 0u64;
202        let mut file_count = 0;
203        if path.exists() {
204            for entry in WalkDir::new(path).into_iter().filter_map(|e| e.ok()) {
205                if entry.file_type().is_file() {
206                    file_count += 1;
207                    size += entry
208                        .metadata()
209                        .map_err(|e| SandboxError::Syscall(e.to_string()))?
210                        .len();
211                }
212            }
213        }
214        Ok(Self {
215            name: name.to_string(),
216            size,
217            file_count,
218            writable,
219        })
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_overlay_config_creation() {
229        let config = OverlayConfig::new("/base", "/upper");
230        assert_eq!(config.lower, PathBuf::from("/base"));
231        assert_eq!(config.upper, PathBuf::from("/upper"));
232    }
233
234    #[test]
235    fn test_overlay_config_mount_options() {
236        let config = OverlayConfig::new("/lower", "/upper");
237        let opts = config.get_mount_options().unwrap();
238        assert!(opts.contains("lowerdir=/lower"));
239        assert!(opts.contains("upperdir=/upper"));
240    }
241
242    #[test]
243    fn test_overlay_fs_creation() {
244        let config = OverlayConfig::new("/base", "/upper");
245        let fs = OverlayFS::new(config);
246        assert!(!fs.is_mounted());
247    }
248
249    #[test]
250    fn test_overlay_paths() {
251        let config = OverlayConfig::new("/lower", "/upper");
252        let fs = OverlayFS::new(config);
253        assert_eq!(fs.lower_path(), Path::new("/lower"));
254        assert_eq!(fs.upper_path(), Path::new("/upper"));
255    }
256}