1use crate::config::Config;
2use crate::error::{ErrorCode, MitosisError, Result};
3use crate::paths::Home;
4use std::ffi::CString;
5use std::fs::{Metadata, OpenOptions, Permissions};
6use std::os::unix::ffi::OsStrExt;
7use std::os::unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt, PermissionsExt};
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11mod copy;
12pub use copy::{CopyBackend, SLOW_WARNING};
13#[cfg(target_os = "linux")]
14mod reflink;
15#[cfg(target_os = "linux")]
16pub use reflink::ReflinkBackend;
17#[cfg(target_os = "linux")]
18pub mod zfs;
19#[cfg(target_os = "linux")]
20pub use zfs::ZfsBackend;
21
22#[cfg(target_os = "macos")]
24const CLONE_NOFOLLOW: u32 = 0x0001;
25
26pub trait CloneBackend: Send + Sync {
27 fn name(&self) -> &'static str;
29 fn check_supported(&self, path: &Path) -> Result<()>;
30 fn clone_dir(&self, src: &Path, dst: &Path) -> Result<()>;
32 fn copy_dir(&self, src: &Path, dst: &Path) -> Result<()> {
35 self.clone_dir(src, dst)
36 }
37 fn create_dir(&self, path: &Path) -> Result<()>;
39 fn remove_dir(&self, path: &Path) -> Result<()>;
41 fn rename_dir(&self, from: &Path, to: &Path) -> Result<()>;
43 fn private_size(&self, path: &Path) -> Option<u64>;
45 fn take_kept(&self) -> Vec<PathBuf> {
48 Vec::new()
49 }
50 fn external_mountpoint(&self, _dataset: &str) -> Result<PathBuf> {
52 Err(no_running_source(self.name()))
53 }
54 fn snapshot_external(&self, _dataset: &str, _name: &str) -> Result<PathBuf> {
56 Err(no_running_source(self.name()))
57 }
58 fn destroy_external(&self, _dataset: &str, _name: &str) -> Result<()> {
60 Err(no_running_source(self.name()))
61 }
62}
63
64fn no_running_source(backend: &str) -> MitosisError {
65 unsupported(
66 format!("--from-running needs the zfs back end; this home uses {backend}"),
67 "set backend = zfs and zfs_dataset, with MITOSIS_HOME on that dataset",
68 )
69}
70
71pub(crate) fn fill_copy(src: &Path, dst: &Path) -> Result<()> {
74 fill_tree(src, dst, ©::copy_file)
75}
76
77fn fs_create_dir(path: &Path) -> Result<()> {
81 std::fs::DirBuilder::new()
82 .recursive(true)
83 .mode(0o700)
84 .create(path)
85 .map_err(|e| {
86 MitosisError::new(
87 ErrorCode::Internal,
88 format!("create {}: {e}", path.display()),
89 )
90 })
91}
92
93fn fs_remove_dir(path: &Path) -> Result<()> {
94 match std::fs::remove_dir_all(path) {
95 Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(MitosisError::new(
96 ErrorCode::Internal,
97 format!("remove {}: {e}", path.display()),
98 )),
99 _ => Ok(()),
100 }
101}
102
103fn fs_rename_dir(from: &Path, to: &Path) -> Result<()> {
105 Ok(std::fs::rename(from, to)?)
106}
107
108#[cfg(target_os = "macos")]
109pub struct ApfsBackend;
110
111#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113pub enum BackendKind {
114 Auto,
115 Apfs,
116 Reflink,
117 Zfs,
118 Copy,
119}
120
121impl BackendKind {
122 pub fn as_str(self) -> &'static str {
123 match self {
124 BackendKind::Auto => "auto",
125 BackendKind::Apfs => "apfs",
126 BackendKind::Reflink => "reflink",
127 BackendKind::Zfs => "zfs",
128 BackendKind::Copy => "copy",
129 }
130 }
131}
132
133impl std::str::FromStr for BackendKind {
134 type Err = MitosisError;
135
136 fn from_str(s: &str) -> Result<Self> {
137 Ok(match s {
138 "auto" => BackendKind::Auto,
139 "apfs" => BackendKind::Apfs,
140 "reflink" => BackendKind::Reflink,
141 "zfs" => BackendKind::Zfs,
142 "copy" => BackendKind::Copy,
143 _ => {
144 return Err(MitosisError::new(
145 ErrorCode::InvalidRequest,
146 format!("unknown back end {s:?}"),
147 )
148 .with_hint("backends are: auto, apfs, reflink, zfs, copy"))
149 }
150 })
151 }
152}
153
154fn unsupported(msg: impl Into<String>, hint: impl Into<String>) -> MitosisError {
155 MitosisError::new(ErrorCode::BackendUnsupported, msg).with_hint(hint)
156}
157
158#[cfg(not(target_os = "macos"))]
160fn fs_type_name(path: &Path) -> String {
161 let Ok(c) = cpath(path) else {
162 return "unknown".into();
163 };
164 let mut st: libc::statfs = unsafe { std::mem::zeroed() };
165 if unsafe { libc::statfs(c.as_ptr(), &mut st) } != 0 {
166 return "unknown".into();
167 }
168 let magic = st.f_type as u64 & 0xFFFF_FFFF;
169 match magic {
170 0x9123683E => "btrfs".into(),
171 0x5846_5342 => "xfs".into(),
172 0xEF53 => "ext4".into(),
173 0x2FC1_2FC1 => "zfs".into(),
174 0x0102_1994 => "tmpfs".into(),
175 0x794C_7630 => "overlayfs".into(),
176 m => format!("filesystem 0x{m:x}"),
177 }
178}
179
180pub fn select_backend(home: &Home, cfg: &Config) -> Result<Arc<dyn CloneBackend>> {
183 match cfg.backend {
184 #[cfg(target_os = "macos")]
185 BackendKind::Auto | BackendKind::Apfs => Ok(Arc::new(ApfsBackend)),
186 #[cfg(not(target_os = "macos"))]
187 BackendKind::Apfs => Err(unsupported(
188 "the apfs back end only works on macOS",
189 "set backend = \"auto\", or put MITOSIS_HOME on Btrfs, XFS (mkfs.xfs -m reflink=1) or ZFS",
190 )),
191 #[cfg(not(target_os = "macos"))]
192 BackendKind::Auto => {
193 #[cfg(target_os = "linux")]
197 let zfs_err = match cfg.zfs_dataset.as_deref().map(ZfsBackend::new) {
198 Some(Ok(zfs)) if zfs.contains(&home.root) => return Ok(Arc::new(zfs)),
199 Some(Err(e)) => Some(e),
200 _ => None,
201 };
202 let fs = fs_type_name(&home.root);
203 let no_backend = || {
204 unsupported(
205 format!(
206 "no clone back end works on the '{fs}' file system at {}",
207 home.root.display()
208 ),
209 "put MITOSIS_HOME on Btrfs, XFS (mkfs.xfs -m reflink=1) or ZFS, or set backend = \"copy\"",
210 )
211 };
212 #[cfg(target_os = "linux")]
213 match ReflinkBackend.check_supported(&home.root) {
214 Ok(()) => return Ok(Arc::new(ReflinkBackend)),
215 Err(e) if e.code != ErrorCode::BackendUnsupported => return Err(e),
216 Err(_) => {}
217 }
218 #[cfg(target_os = "linux")]
219 if let (Some(e), "zfs") = (zfs_err, fs.as_str()) {
220 return Err(e);
221 }
222 if fs == "zfs" {
223 return Err(unsupported(
224 format!(
225 "{} is on ZFS without block cloning, and no zfs_dataset holds it",
226 home.root.display()
227 ),
228 "set zfs_dataset to the dataset whose mountpoint holds MITOSIS_HOME, for example: mitosis config set zfs_dataset tank/mitosis",
229 ));
230 }
231 Err(no_backend())
232 }
233 #[cfg(target_os = "linux")]
234 BackendKind::Reflink => Ok(Arc::new(ReflinkBackend)),
235 #[cfg(not(target_os = "linux"))]
236 BackendKind::Reflink => Err(unsupported(
237 "the reflink back end only works on Linux",
238 "set backend = \"auto\"",
239 )),
240 BackendKind::Copy => Ok(Arc::new(CopyBackend)),
241 #[cfg(target_os = "linux")]
242 BackendKind::Zfs => {
243 let ds = cfg.zfs_dataset.as_deref().ok_or_else(|| {
244 unsupported(
245 "backend = \"zfs\" needs zfs_dataset",
246 "set zfs_dataset to the dataset Mitosis owns, for example: mitosis config set zfs_dataset tank/mitosis",
247 )
248 })?;
249 Ok(Arc::new(ZfsBackend::new(ds)?))
250 }
251 #[cfg(not(target_os = "linux"))]
252 BackendKind::Zfs => {
253 let _ = home;
254 Err(unsupported("the zfs back end only works on Linux", "set backend = \"auto\""))
255 }
256 }
257}
258
259fn cpath(p: &Path) -> Result<CString> {
260 CString::new(p.as_os_str().as_bytes()).map_err(|_| {
261 MitosisError::new(
262 ErrorCode::Internal,
263 format!("path contains a NUL byte: {}", p.display()),
264 )
265 })
266}
267
268#[cfg(target_os = "macos")]
269impl CloneBackend for ApfsBackend {
270 fn name(&self) -> &'static str {
271 "apfs"
272 }
273
274 fn check_supported(&self, path: &Path) -> Result<()> {
275 let c = cpath(path)?;
276 let mut st: libc::statfs = unsafe { std::mem::zeroed() };
277 if unsafe { libc::statfs(c.as_ptr(), &mut st) } != 0 {
278 let e = std::io::Error::last_os_error();
279 return Err(MitosisError::new(
280 ErrorCode::Internal,
281 format!("statfs {}: {e}", path.display()),
282 ));
283 }
284 let name = unsafe { std::ffi::CStr::from_ptr(st.f_fstypename.as_ptr()) }.to_string_lossy();
285 if name != "apfs" {
286 return Err(MitosisError::new(
287 ErrorCode::NotApfs,
288 format!("{} is on a '{name}' file system, not APFS", path.display()),
289 )
290 .with_hint("set MITOSIS_HOME to a folder on an APFS volume"));
291 }
292 Ok(())
293 }
294
295 fn clone_dir(&self, src: &Path, dst: &Path) -> Result<()> {
296 let (s, d) = (cpath(src)?, cpath(dst)?);
297 if unsafe { libc::clonefile(s.as_ptr(), d.as_ptr(), CLONE_NOFOLLOW) } == 0 {
298 return Ok(());
299 }
300 let e = std::io::Error::last_os_error();
301 let msg = format!("clonefile {} -> {}: {e}", src.display(), dst.display());
302 Err(match e.raw_os_error() {
303 Some(libc::EXDEV) => MitosisError::new(ErrorCode::CrossVolume, msg)
304 .with_hint("source and destination must be on the same volume"),
305 _ => MitosisError::new(ErrorCode::Internal, msg),
306 })
307 }
308
309 fn create_dir(&self, path: &Path) -> Result<()> {
310 fs_create_dir(path)
311 }
312
313 fn remove_dir(&self, path: &Path) -> Result<()> {
314 fs_remove_dir(path)
315 }
316
317 fn rename_dir(&self, from: &Path, to: &Path) -> Result<()> {
318 fs_rename_dir(from, to)
319 }
320
321 fn private_size(&self, path: &Path) -> Option<u64> {
322 apfs_private_size(path)
323 }
324}
325
326fn stat(path: &Path) -> Result<Metadata> {
327 std::fs::metadata(path).map_err(|e| {
328 MitosisError::new(ErrorCode::Internal, format!("stat {}: {e}", path.display()))
329 })
330}
331
332pub fn same_volume(a: &Path, b: &Path) -> Result<bool> {
333 Ok(stat(a)?.dev() == stat(b)?.dev())
334}
335
336pub fn free_space(path: &Path) -> Result<u64> {
337 let c = cpath(path)?;
338 let mut st: libc::statvfs = unsafe { std::mem::zeroed() };
339 if unsafe { libc::statvfs(c.as_ptr(), &mut st) } != 0 {
340 let e = std::io::Error::last_os_error();
341 return Err(MitosisError::new(
342 ErrorCode::Internal,
343 format!("statvfs {}: {e}", path.display()),
344 ));
345 }
346 Ok(st.f_bavail as u64 * st.f_frsize as u64)
347}
348
349fn walk_files(
351 path: &Path,
352 f: &mut dyn FnMut(&Path, &Metadata) -> Option<()>,
353) -> Result<Option<()>> {
354 let md = std::fs::symlink_metadata(path).map_err(|e| {
355 MitosisError::new(
356 ErrorCode::Internal,
357 format!("lstat {}: {e}", path.display()),
358 )
359 })?;
360 if md.is_dir() {
361 let rd = std::fs::read_dir(path).map_err(|e| {
362 MitosisError::new(
363 ErrorCode::Internal,
364 format!("read_dir {}: {e}", path.display()),
365 )
366 })?;
367 for entry in rd {
368 let entry = entry?;
369 if walk_files(&entry.path(), f)?.is_none() {
370 return Ok(None);
371 }
372 }
373 Ok(Some(()))
374 } else if md.is_file() {
375 Ok(f(path, &md))
376 } else {
377 Ok(Some(())) }
379}
380
381fn io_error(what: &str, path: &Path, e: std::io::Error) -> MitosisError {
382 MitosisError::new(
383 ErrorCode::Internal,
384 format!("{what} {}: {e}", path.display()),
385 )
386}
387
388fn clone_tree(
391 src: &Path,
392 dst: &Path,
393 file: &dyn Fn(&Path, &Path, &Metadata) -> Result<()>,
394) -> Result<()> {
395 if std::fs::symlink_metadata(dst).is_ok() {
396 return Err(MitosisError::new(
397 ErrorCode::Internal,
398 format!(
399 "clone {} -> {}: destination exists",
400 src.display(),
401 dst.display()
402 ),
403 ));
404 }
405 clone_entry(src, dst, file).inspect_err(|_| {
406 let _ = std::fs::remove_dir_all(dst).or_else(|_| std::fs::remove_file(dst));
407 })
408}
409
410fn fill_tree(
413 src: &Path,
414 dst: &Path,
415 file: &dyn Fn(&Path, &Path, &Metadata) -> Result<()>,
416) -> Result<()> {
417 let md = std::fs::symlink_metadata(src).map_err(|e| io_error("lstat", src, e))?;
418 for entry in std::fs::read_dir(src).map_err(|e| io_error("read_dir", src, e))? {
419 let entry = entry.map_err(|e| io_error("read_dir", src, e))?;
420 clone_entry(&entry.path(), &dst.join(entry.file_name()), file)?;
421 }
422 keep_owner(dst, &md)?;
423 std::fs::set_permissions(dst, Permissions::from_mode(md.mode() & 0o1777))
424 .map_err(|e| io_error("chmod", dst, e))?;
425 keep_times(dst, &md)
426}
427
428fn create_dst(dst: &Path, md: &Metadata) -> Result<std::fs::File> {
430 OpenOptions::new()
431 .write(true)
432 .create_new(true)
433 .mode(md.mode() & 0o777)
434 .open(dst)
435 .map_err(|e| io_error("create", dst, e))
436}
437
438fn clone_entry(
441 src: &Path,
442 dst: &Path,
443 file: &dyn Fn(&Path, &Path, &Metadata) -> Result<()>,
444) -> Result<()> {
445 let md = std::fs::symlink_metadata(src).map_err(|e| io_error("lstat", src, e))?;
446 let ft = md.file_type();
447 if ft.is_symlink() {
448 let target = std::fs::read_link(src).map_err(|e| io_error("readlink", src, e))?;
449 std::os::unix::fs::symlink(&target, dst).map_err(|e| io_error("symlink", dst, e))?;
450 } else if ft.is_dir() {
451 std::fs::DirBuilder::new()
453 .mode(0o700)
454 .create(dst)
455 .map_err(|e| io_error("mkdir", dst, e))?;
456 for entry in std::fs::read_dir(src).map_err(|e| io_error("read_dir", src, e))? {
457 let entry = entry.map_err(|e| io_error("read_dir", src, e))?;
458 clone_entry(&entry.path(), &dst.join(entry.file_name()), file)?;
459 }
460 } else if ft.is_file() {
461 file(src, dst, &md)?;
462 } else {
463 return Ok(());
464 }
465 keep_owner(dst, &md)?;
466 if !ft.is_symlink() {
467 std::fs::set_permissions(dst, Permissions::from_mode(md.mode() & 0o1777))
469 .map_err(|e| io_error("chmod", dst, e))?;
470 }
471 keep_times(dst, &md)
472}
473
474fn keep_owner(path: &Path, md: &Metadata) -> Result<()> {
476 let (uid, gid) = unsafe { (libc::geteuid(), libc::getegid()) };
477 if md.uid() == uid && md.gid() == gid {
478 return Ok(());
479 }
480 let c = cpath(path)?;
481 if unsafe { libc::lchown(c.as_ptr(), md.uid(), md.gid()) } == 0 {
482 return Ok(());
483 }
484 let e = std::io::Error::last_os_error();
485 match e.raw_os_error() {
486 Some(libc::EPERM) => Ok(()), _ => Err(io_error("chown", path, e)),
488 }
489}
490
491fn keep_times(path: &Path, md: &Metadata) -> Result<()> {
493 let c = cpath(path)?;
494 let mut times: [libc::timespec; 2] = unsafe { std::mem::zeroed() };
495 times[0].tv_sec = md.atime() as libc::time_t;
496 times[0].tv_nsec = md.atime_nsec() as _;
497 times[1].tv_sec = md.mtime() as libc::time_t;
498 times[1].tv_nsec = md.mtime_nsec() as _;
499 let rc = unsafe {
500 libc::utimensat(
501 libc::AT_FDCWD,
502 c.as_ptr(),
503 times.as_ptr(),
504 libc::AT_SYMLINK_NOFOLLOW,
505 )
506 };
507 if rc != 0 {
508 return Err(io_error("utimensat", path, std::io::Error::last_os_error()));
509 }
510 Ok(())
511}
512
513pub fn apparent_size(path: &Path) -> Result<u64> {
514 let mut total = 0;
515 walk_files(path, &mut |_, md| {
516 total += md.len();
517 Some(())
518 })?;
519 Ok(total)
520}
521
522#[cfg(target_os = "macos")]
524fn file_private_size(path: &Path) -> Option<u64> {
525 #[repr(C, packed)]
526 struct Reply {
527 length: u32,
528 private_size: i64, }
530 let c = cpath(path).ok()?;
531 let mut al: libc::attrlist = unsafe { std::mem::zeroed() };
532 al.bitmapcount = libc::ATTR_BIT_MAP_COUNT;
533 al.forkattr = libc::ATTR_CMNEXT_PRIVATESIZE; let mut reply = Reply {
535 length: 0,
536 private_size: 0,
537 };
538 let rc = unsafe {
539 libc::getattrlist(
540 c.as_ptr(),
541 &mut al as *mut _ as *mut libc::c_void,
542 &mut reply as *mut _ as *mut libc::c_void,
543 std::mem::size_of::<Reply>(),
544 libc::FSOPT_NOFOLLOW | libc::FSOPT_ATTR_CMN_EXTENDED,
545 )
546 };
547 let size = reply.private_size;
548 (rc == 0 && size >= 0).then_some(size as u64)
549}
550
551#[cfg(target_os = "macos")]
554fn apfs_private_size(path: &Path) -> Option<u64> {
555 let mut total = 0u64;
556 let done = walk_files(path, &mut |p, _| {
557 total += file_private_size(p)?;
558 Some(())
559 });
560 matches!(done, Ok(Some(()))).then_some(total)
561}
562
563#[cfg(all(test, target_os = "macos"))]
564mod tests {
565 use super::*;
566 use std::fs::{create_dir, read_to_string, write};
567
568 #[test]
569 fn auto_picks_apfs_on_apfs() {
570 let tmp = tempfile::tempdir().unwrap();
571 let home = Home::at(tmp.path().to_path_buf());
572 let b = select_backend(&home, &Config::default()).unwrap();
573 assert_eq!(b.name(), "apfs");
574 b.check_supported(tmp.path()).unwrap();
575 }
576
577 #[test]
578 fn unbuilt_backends_are_unsupported() {
579 let tmp = tempfile::tempdir().unwrap();
580 let home = Home::at(tmp.path().to_path_buf());
581 for kind in [BackendKind::Reflink, BackendKind::Zfs] {
582 let cfg = Config {
583 backend: kind,
584 ..Config::default()
585 };
586 let e = select_backend(&home, &cfg).err().unwrap();
587 assert_eq!(e.code, ErrorCode::BackendUnsupported);
588 }
589 }
590
591 #[test]
592 fn clone_is_isolated_and_shares_blocks() {
593 let tmp = tempfile::tempdir().unwrap();
594 ApfsBackend.check_supported(tmp.path()).unwrap();
595 let (src, dst) = (tmp.path().join("src"), tmp.path().join("dst"));
596 create_dir(&src).unwrap();
597 write(src.join("a.txt"), "hello").unwrap();
598 create_dir(src.join("sub")).unwrap();
599 write(src.join("sub/b.txt"), "x".repeat(1_000_000)).unwrap();
600
601 ApfsBackend.clone_dir(&src, &dst).unwrap();
602 assert_eq!(read_to_string(dst.join("a.txt")).unwrap(), "hello");
603 write(dst.join("a.txt"), "changed").unwrap();
604 assert_eq!(read_to_string(src.join("a.txt")).unwrap(), "hello");
605
606 assert!(same_volume(&src, &dst).unwrap());
607 assert!(free_space(tmp.path()).unwrap() > 0);
608 assert!(ApfsBackend.clone_dir(&src, &dst).is_err());
609 assert_eq!(apparent_size(&src).unwrap(), 1_000_005);
610 if let Some(p) = ApfsBackend.private_size(&dst) {
611 assert!(p < 1_000_000);
612 }
613 }
614}
615
616#[cfg(test)]
617mod shared_tests {
618 use super::*;
619 use std::fs::{create_dir, write};
620
621 #[test]
622 fn symlinks_are_not_followed() {
623 let tmp = tempfile::tempdir().unwrap();
624 let big = tmp.path().join("big");
625 write(&big, vec![0u8; 4096]).unwrap();
626 let dir = tmp.path().join("d");
627 create_dir(&dir).unwrap();
628 std::os::unix::fs::symlink(&big, dir.join("link")).unwrap();
629 assert_eq!(apparent_size(&dir).unwrap(), 0);
630 }
631}
632
633#[cfg(all(test, not(target_os = "macos")))]
634mod auto_tests {
635 use super::*;
636
637 #[test]
638 fn auto_never_picks_copy() {
639 let tmp = tempfile::tempdir_in(std::env::var("MITOSIS_TEST_ROOT").unwrap_or("/tmp".into()))
640 .unwrap();
641 let home = Home::at(tmp.path().to_path_buf());
642 if let Ok(b) = select_backend(&home, &Config::default()) {
644 assert_ne!(b.name(), "copy");
645 return;
646 }
647 let e = select_backend(&home, &Config::default()).err().unwrap();
648 assert_eq!(e.code, ErrorCode::BackendUnsupported);
649 }
650}