sandbox_rs/storage/
filesystem.rs1use crate::errors::{Result, SandboxError};
4use std::fs;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone)]
9pub struct OverlayConfig {
10 pub lower: PathBuf,
12 pub upper: PathBuf,
14 pub work: PathBuf,
16 pub merged: PathBuf,
18}
19
20impl OverlayConfig {
21 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 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 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 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
98pub struct OverlayFS {
100 config: OverlayConfig,
101 mounted: bool,
102}
103
104impl OverlayFS {
105 pub fn new(config: OverlayConfig) -> Self {
107 Self {
108 config,
109 mounted: false,
110 }
111 }
112
113 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 pub fn is_mounted(&self) -> bool {
160 self.mounted
161 }
162
163 pub fn merged_path(&self) -> &Path {
165 &self.config.merged
166 }
167
168 pub fn upper_path(&self) -> &Path {
170 &self.config.upper
171 }
172
173 pub fn lower_path(&self) -> &Path {
175 &self.config.lower
176 }
177
178 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 let _ = fs::remove_dir_all(&self.config.work);
207
208 Ok(())
209 }
210
211 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#[derive(Debug, Clone)]
235pub struct LayerInfo {
236 pub name: String,
238 pub size: u64,
240 pub file_count: usize,
242 pub writable: bool,
244}
245
246impl LayerInfo {
247 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}