sandbox_rs/storage/
filesystem.rs

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