// This file is part of the uutils coreutils package. // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore reflink use std::ffi::CString; use std::fs::{self, File, OpenOptions}; use std::os::unix::ffi::OsStrExt; use std::os::unix::fs::OpenOptionsExt; use std::path::Path; use quick_error::ResultExt; use uucore::buf_copy; use uucore::mode::get_umask; use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; /// Copies `source` to `dest` using copy-on-write if possible. /// /// The `source_is_fifo` flag must be set to `true` if and only if /// `source` is a FIFO (also known as a named pipe). pub(crate) fn copy_on_write( source: &Path, dest: &Path, reflink_mode: ReflinkMode, sparse_mode: SparseMode, context: &str, source_is_fifo: bool, source_is_stream: bool, ) -> CopyResult { if sparse_mode != SparseMode::Auto { return Err("--sparse is only supported on linux".to_string().into()); } let mut copy_debug = CopyDebug { offload: OffloadReflinkDebug::Unknown, reflink: OffloadReflinkDebug::Unsupported, sparse_detection: SparseDebug::Unsupported, }; // 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. 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 // Only remove the `dest` if the `source` and `dest` are not the same && source != dest { // 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. // first lets make sure the dest file is not read only if fs::metadata(dest).is_ok_and(|md| !md.permissions().readonly()) { // remove and copy again // TODO: rewrite this to better match linux behavior // linux first opens the source file and destination file then uses the file // descriptors to do the clone. let _ = fs::remove_file(dest); error = pfn(src.as_ptr(), dst.as_ptr(), 0); } } } } if raw_pfn.is_null() || error != 0 { // clonefile(2) is either not supported or it errored out (possibly because the FS does not // support COW). match reflink_mode { ReflinkMode::Always => { return Err(format!("failed to clone {source:?} from {dest:?}: {error}").into()); } _ => { copy_debug.reflink = OffloadReflinkDebug::Yes; if source_is_stream { let mut src_file = File::open(source)?; let mode = 0o622 & !get_umask(); let mut dst_file = OpenOptions::new() .create(true) .write(true) .mode(mode) .open(dest)?; let context = buf_copy::copy_stream(&mut src_file, &mut dst_file) .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other)) .context(context)?; if source_is_fifo { dst_file.set_permissions(src_file.metadata()?.permissions())?; } context } else { fs::copy(source, dest).context(context)? } } }; } Ok(copy_debug) }