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        fs::create_dir_all(&self.work).map_err(|e| {
60            SandboxError::Syscall(format!("Failed to create work directory: {}", e))
61        })?;
62
63        fs::create_dir_all(&self.merged).map_err(|e| {
64            SandboxError::Syscall(format!("Failed to create merged directory: {}", e))
65        })?;
66
67        Ok(())
68    }
69
70    /// Get overlay mount string for mount command
71    pub fn get_mount_options(&self) -> String {
72        format!(
73            "lowerdir={},upperdir={},workdir={}",
74            self.lower.display(),
75            self.upper.display(),
76            self.work.display()
77        )
78    }
79}
80
81/// Overlay filesystem manager
82pub struct OverlayFS {
83    config: OverlayConfig,
84    mounted: bool,
85}
86
87impl OverlayFS {
88    /// Create new overlay filesystem
89    pub fn new(config: OverlayConfig) -> Self {
90        Self {
91            config,
92            mounted: false,
93        }
94    }
95
96    /// Setup overlay filesystem
97    pub fn setup(&mut self) -> Result<()> {
98        self.config.validate()?;
99        self.config.setup_directories()?;
100
101        // TODO: Actual mount would require root and real mount syscall
102        self.mounted = true;
103        Ok(())
104    }
105
106    /// Check if filesystem is mounted
107    pub fn is_mounted(&self) -> bool {
108        self.mounted
109    }
110
111    /// Get merged (visible) directory
112    pub fn merged_path(&self) -> &Path {
113        &self.config.merged
114    }
115
116    /// Get upper (writable) directory
117    pub fn upper_path(&self) -> &Path {
118        &self.config.upper
119    }
120
121    /// Get lower (read-only) directory
122    pub fn lower_path(&self) -> &Path {
123        &self.config.lower
124    }
125
126    /// Cleanup overlay filesystem
127    pub fn cleanup(&mut self) -> Result<()> {
128        if self.mounted {
129            // Unmount would go here
130            self.mounted = false;
131        }
132
133        // Clean up work directory
134        let _ = fs::remove_dir_all(&self.config.work);
135
136        Ok(())
137    }
138
139    /// Get total size of changes in upper layer
140    pub fn get_changes_size(&self) -> Result<u64> {
141        let mut total = 0u64;
142
143        for entry in fs::read_dir(&self.config.upper)
144            .map_err(|e| SandboxError::Syscall(format!("Cannot read upper layer: {}", e)))?
145        {
146            let entry = entry
147                .map_err(|e| SandboxError::Syscall(format!("Directory entry error: {}", e)))?;
148            let path = entry.path();
149
150            if path.is_file() {
151                total += entry
152                    .metadata()
153                    .map_err(|e| SandboxError::Syscall(e.to_string()))?
154                    .len();
155            }
156        }
157
158        Ok(total)
159    }
160}
161
162/// File layer information
163#[derive(Debug, Clone)]
164pub struct LayerInfo {
165    /// Layer name
166    pub name: String,
167    /// Layer size in bytes
168    pub size: u64,
169    /// Number of files
170    pub file_count: usize,
171    /// Whether layer is writable
172    pub writable: bool,
173}
174
175impl LayerInfo {
176    /// Get layer info from path
177    pub fn from_path(name: &str, path: &Path, writable: bool) -> Result<Self> {
178        let mut size = 0u64;
179        let mut file_count = 0;
180
181        if path.exists() {
182            for entry in fs::read_dir(path)
183                .map_err(|e| SandboxError::Syscall(format!("Cannot read layer: {}", e)))?
184            {
185                let entry = entry.map_err(|e| SandboxError::Syscall(e.to_string()))?;
186
187                if entry.path().is_file() {
188                    file_count += 1;
189                    size += entry
190                        .metadata()
191                        .map_err(|e| SandboxError::Syscall(e.to_string()))?
192                        .len();
193                }
194            }
195        }
196
197        Ok(Self {
198            name: name.to_string(),
199            size,
200            file_count,
201            writable,
202        })
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_overlay_config_creation() {
212        let config = OverlayConfig::new("/base", "/upper");
213        assert_eq!(config.lower, PathBuf::from("/base"));
214        assert_eq!(config.upper, PathBuf::from("/upper"));
215    }
216
217    #[test]
218    fn test_overlay_config_mount_options() {
219        let config = OverlayConfig::new("/lower", "/upper");
220        let opts = config.get_mount_options();
221
222        assert!(opts.contains("lowerdir="));
223        assert!(opts.contains("upperdir="));
224        assert!(opts.contains("workdir="));
225    }
226
227    #[test]
228    fn test_overlay_fs_creation() {
229        let config = OverlayConfig::new("/base", "/upper");
230        let fs = OverlayFS::new(config);
231
232        assert!(!fs.is_mounted());
233    }
234
235    #[test]
236    fn test_layer_info_size_calculation() {
237        let info = LayerInfo {
238            name: "test".to_string(),
239            size: 1024,
240            file_count: 5,
241            writable: true,
242        };
243
244        assert_eq!(info.size, 1024);
245        assert_eq!(info.file_count, 5);
246        assert!(info.writable);
247    }
248
249    #[test]
250    fn test_overlay_paths() {
251        let config = OverlayConfig::new("/lower", "/upper");
252        let fs = OverlayFS::new(config);
253
254        assert_eq!(fs.lower_path(), Path::new("/lower"));
255        assert_eq!(fs.upper_path(), Path::new("/upper"));
256    }
257}