1use sandbox_core::{Result, SandboxError};
4use std::fs;
5use std::path::{Path, PathBuf};
6
7#[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
83pub 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#[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}