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