From 4bf33e98a8153891bb3b745b700e96bee0901506 Mon Sep 17 00:00:00 2001 From: Nicolas Thery Date: Sat, 24 Apr 2021 19:20:31 +0200 Subject: [PATCH] cp: add --reflink support for macOS Fixes #1773 --- src/uu/cp/src/cp.rs | 68 +++++++++++++++++++++++++++++++++++++--- tests/by-util/test_cp.rs | 8 ++--- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 519590e85..ca564e37c 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -1193,12 +1193,17 @@ fn copy_file(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> { Ok(()) } -///Copy the file from `source` to `dest` either using the normal `fs::copy` or the -///`FICLONE` ioctl if --reflink is specified and the filesystem supports it. +/// Copy the file from `source` to `dest` either using the normal `fs::copy` or a +/// copy-on-write scheme if --reflink is specified and the filesystem supports it. fn copy_helper(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> { if options.reflink_mode != ReflinkMode::Never { - #[cfg(not(target_os = "linux"))] - return Err("--reflink is only supported on linux".to_string().into()); + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + return Err("--reflink is only supported on linux and macOS" + .to_string() + .into()); + + #[cfg(target_os = "macos")] + copy_on_write_macos(source, dest, options.reflink_mode)?; #[cfg(target_os = "linux")] copy_on_write_linux(source, dest, options.reflink_mode)?; @@ -1274,6 +1279,61 @@ fn copy_on_write_linux(source: &Path, dest: &Path, mode: ReflinkMode) -> CopyRes Ok(()) } +/// Copies `source` to `dest` using copy-on-write if possible. +#[cfg(target_os = "macos")] +fn copy_on_write_macos(source: &Path, dest: &Path, mode: ReflinkMode) -> CopyResult<()> { + debug_assert!(mode != ReflinkMode::Never); + + // Extract paths in a form suitable to be passed to a syscall. + // The unwrap() is safe because they come from the command-line and so contain non nul + // character. + use std::os::unix::ffi::OsStrExt; + let src = CString::new(source.as_os_str().as_bytes()).unwrap(); + let dst = CString::new(dest.as_os_str().as_bytes()).unwrap(); + + // clonefile(2) was introduced in macOS 10.12 so we cannot statically link against it + // for backward compatibility. + let clonefile = CString::new("clonefile").unwrap(); + let raw_pfn = unsafe { libc::dlsym(libc::RTLD_NEXT, clonefile.as_ptr()) }; + + let mut error = 0; + if !raw_pfn.is_null() { + // Call clonefile(2). + // Safety: Casting a C function pointer to a rust function value is one of the few + // blessed uses of `transmute()`. + unsafe { + let pfn: extern "C" fn( + src: *const libc::c_char, + dst: *const libc::c_char, + flags: u32, + ) -> libc::c_int = std::mem::transmute(raw_pfn); + error = pfn(src.as_ptr(), dst.as_ptr(), 0); + if std::io::Error::last_os_error().kind() == std::io::ErrorKind::AlreadyExists { + // clonefile(2) fails if the destination exists. Remove it and try again. Do not + // bother to check if removal worked because we're going to try to clone again. + let _ = fs::remove_file(dest); + error = pfn(src.as_ptr(), dst.as_ptr(), 0); + } + } + } + + if raw_pfn.is_null() || error != 0 { + // clonefile(2) is not supported or it error'ed out (possibly because the FS does not + // support COW). + match mode { + ReflinkMode::Always => { + return Err( + format!("failed to clone {:?} from {:?}: {}", source, dest, error).into(), + ) + } + ReflinkMode::Auto => fs::copy(source, dest).context(&*context_for(source, dest))?, + ReflinkMode::Never => unreachable!(), + }; + } + + Ok(()) +} + /// Generate an error message if `target` is not the correct `target_type` pub fn verify_target_type(target: &Path, target_type: &TargetType) -> CopyResult<()> { match (target_type, target.is_dir()) { diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 53c16e677..1e99da0fb 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -967,7 +967,7 @@ fn test_cp_one_file_system() { } #[test] -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "macos"))] fn test_cp_reflink_always() { let (at, mut ucmd) = at_and_ucmd!(); let result = ucmd @@ -985,7 +985,7 @@ fn test_cp_reflink_always() { } #[test] -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "macos"))] fn test_cp_reflink_auto() { let (at, mut ucmd) = at_and_ucmd!(); ucmd.arg("--reflink=auto") @@ -998,7 +998,7 @@ fn test_cp_reflink_auto() { } #[test] -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "macos"))] fn test_cp_reflink_never() { let (at, mut ucmd) = at_and_ucmd!(); ucmd.arg("--reflink=never") @@ -1011,7 +1011,7 @@ fn test_cp_reflink_never() { } #[test] -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "macos"))] fn test_cp_reflink_bad() { let (_, mut ucmd) = at_and_ucmd!(); let _result = ucmd