libfuse_fs/util/
bind_mount.rs1use std::io::{Error, Result};
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8use tokio::sync::Mutex;
9use tracing::{debug, error, info};
10
11#[derive(Debug, Clone)]
13pub struct BindMount {
14 pub source: PathBuf,
16 pub target: PathBuf,
18}
19
20impl BindMount {
21 pub fn parse(spec: &str) -> Result<Self> {
23 let parts: Vec<&str> = spec.split(':').collect();
24 if parts.len() != 2 {
25 return Err(Error::other(format!(
26 "Invalid bind mount spec: '{}'. Expected format: 'source:target'",
27 spec
28 )));
29 }
30
31 let source = PathBuf::from(parts[0]);
32 let target = PathBuf::from(parts[1]);
33
34 let source = if source.is_relative() {
36 PathBuf::from("/").join(source)
37 } else {
38 source
39 };
40
41 Ok(BindMount { source, target })
42 }
43}
44
45pub struct BindMountManager {
47 mounts: Arc<Mutex<Vec<MountPoint>>>,
48 mountpoint: PathBuf,
49}
50
51#[derive(Debug)]
52struct MountPoint {
53 target: PathBuf,
54 mounted: bool,
55}
56
57impl BindMountManager {
58 pub fn new<P: AsRef<Path>>(mountpoint: P) -> Self {
60 Self {
61 mounts: Arc::new(Mutex::new(Vec::new())),
62 mountpoint: mountpoint.as_ref().to_path_buf(),
63 }
64 }
65
66 pub async fn mount_all(&self, bind_specs: &[BindMount]) -> Result<()> {
68 let mut mounts = self.mounts.lock().await;
69
70 for bind in bind_specs {
71 let target_path = self
72 .mountpoint
73 .join(bind.target.strip_prefix("/").unwrap_or(&bind.target));
74
75 let source_metadata = std::fs::metadata(&bind.source)?;
77
78 if !target_path.exists() {
79 if source_metadata.is_file() {
80 if let Some(parent) = target_path.parent() {
82 std::fs::create_dir_all(parent)?;
83 debug!("Created parent directory: {:?}", parent);
84 }
85 std::fs::File::create(&target_path)?;
86 debug!("Created target file: {:?}", target_path);
87 } else {
88 std::fs::create_dir_all(&target_path)?;
90 debug!("Created target directory: {:?}", target_path);
91 }
92 }
93
94 self.do_mount(&bind.source, &target_path)?;
96
97 mounts.push(MountPoint {
98 target: target_path.clone(),
99 mounted: true,
100 });
101
102 info!("Bind mounted {:?} -> {:?}", bind.source, target_path);
103 }
104
105 Ok(())
106 }
107
108 #[cfg(target_os = "linux")]
110 fn do_mount(&self, source: &Path, target: &Path) -> Result<()> {
111 use std::ffi::CString;
112
113 let source_cstr = CString::new(
114 source
115 .to_str()
116 .ok_or_else(|| Error::other(format!("Invalid source path: {:?}", source)))?,
117 )
118 .map_err(|e| Error::other(format!("CString error: {}", e)))?;
119
120 let target_cstr = CString::new(
121 target
122 .to_str()
123 .ok_or_else(|| Error::other(format!("Invalid target path: {:?}", target)))?,
124 )
125 .map_err(|e| Error::other(format!("CString error: {}", e)))?;
126
127 let fstype = CString::new("none").unwrap();
128
129 let ret = unsafe {
130 libc::mount(
131 source_cstr.as_ptr(),
132 target_cstr.as_ptr(),
133 fstype.as_ptr(),
134 libc::MS_BIND | libc::MS_REC,
135 std::ptr::null(),
136 )
137 };
138
139 if ret != 0 {
140 let err = Error::last_os_error();
141 error!("Failed to bind mount {:?} to {:?}: {}", source, target, err);
142 return Err(err);
143 }
144
145 let ret = unsafe {
149 libc::mount(
150 std::ptr::null(),
151 target_cstr.as_ptr(),
152 std::ptr::null(),
153 libc::MS_SLAVE | libc::MS_REC,
154 std::ptr::null(),
155 )
156 };
157
158 if ret != 0 {
159 let err = Error::last_os_error();
160 error!("Failed to set mount propagation for {:?}: {}", target, err);
161 unsafe { libc::umount2(target_cstr.as_ptr(), libc::MNT_DETACH) };
163 return Err(err);
164 }
165
166 Ok(())
167 }
168
169 #[cfg(target_os = "macos")]
170 fn do_mount(&self, _source: &Path, _target: &Path) -> Result<()> {
171 Err(Error::other("Bind mounts are not supported on macOS"))
173 }
174
175 pub async fn unmount_all(&self) -> Result<()> {
177 let mut mounts = self.mounts.lock().await;
178 let mut errors = Vec::new();
179
180 while let Some(mut mount) = mounts.pop() {
182 if mount.mounted {
183 if let Err(e) = self.do_unmount(&mount.target) {
184 error!("Failed to unmount {:?}: {}", mount.target, e);
185 errors.push(e);
186 } else {
187 mount.mounted = false;
188 info!("Unmounted {:?}", mount.target);
189 }
190 }
191 }
192
193 if !errors.is_empty() {
194 return Err(Error::other(format!(
195 "Failed to unmount {} bind mounts",
196 errors.len()
197 )));
198 }
199
200 Ok(())
201 }
202
203 #[cfg(target_os = "linux")]
205 fn do_unmount(&self, target: &Path) -> Result<()> {
206 use std::ffi::CString;
207
208 let target_cstr = CString::new(
209 target
210 .to_str()
211 .ok_or_else(|| Error::other(format!("Invalid target path: {:?}", target)))?,
212 )
213 .map_err(|e| Error::other(format!("CString error: {}", e)))?;
214
215 let ret = unsafe { libc::umount2(target_cstr.as_ptr(), libc::MNT_DETACH) };
216
217 if ret != 0 {
218 let err = Error::last_os_error();
219 if err.raw_os_error() != Some(libc::EINVAL) && err.raw_os_error() != Some(libc::ENOENT)
221 {
222 return Err(err);
223 }
224 }
225
226 Ok(())
227 }
228
229 #[cfg(target_os = "macos")]
230 fn do_unmount(&self, _target: &Path) -> Result<()> {
231 Ok(())
232 }
233}
234
235impl Drop for BindMountManager {
236 fn drop(&mut self) {
237 let mounts = self.mounts.try_lock();
239 if let Ok(mut mounts) = mounts {
240 while let Some(mount) = mounts.pop() {
241 if mount.mounted {
242 let _ = self.do_unmount(&mount.target);
243 }
244 }
245 }
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn test_parse_bind_mount() {
255 let bind = BindMount::parse("proc:/proc").unwrap();
256 assert_eq!(bind.source, PathBuf::from("/proc"));
257 assert_eq!(bind.target, PathBuf::from("/proc"));
258
259 let bind = BindMount::parse("/host/path:/container/path").unwrap();
260 assert_eq!(bind.source, PathBuf::from("/host/path"));
261 assert_eq!(bind.target, PathBuf::from("/container/path"));
262
263 let bind = BindMount::parse("sys:/sys").unwrap();
264 assert_eq!(bind.source, PathBuf::from("/sys"));
265 assert_eq!(bind.target, PathBuf::from("/sys"));
266 }
267
268 #[test]
269 fn test_invalid_bind_mount() {
270 assert!(BindMount::parse("invalid").is_err());
271 assert!(BindMount::parse("too:many:colons").is_err());
272 }
273
274 #[tokio::test]
275 #[cfg(target_os = "macos")]
276 async fn test_bind_mount_macos_fail() {
277 let temp = tempfile::tempdir().unwrap();
283 let source = temp.path().join("source");
284 std::fs::create_dir(&source).unwrap();
285 let target_dir = temp.path().join("target_dir");
286 let manager = BindMountManager::new(&target_dir);
287 let bind = BindMount {
288 source: source.clone(),
289 target: std::path::PathBuf::from("mnt"),
290 };
291
292 let result = manager.mount_all(&[bind]).await;
293 assert!(result.is_err());
294 assert_eq!(
295 result.unwrap_err().to_string(),
296 "Bind mounts are not supported on macOS"
297 );
298 }
299}