diff --git a/src/libstd/fs.rs b/src/libstd/fs.rs index eaf5d619f54..991b45fd4a2 100644 --- a/src/libstd/fs.rs +++ b/src/libstd/fs.rs @@ -1615,8 +1615,8 @@ pub fn rename, Q: AsRef>(from: P, to: Q) -> io::Result<()> /// `O_CLOEXEC` is set for returned file descriptors. /// On Windows, this function currently corresponds to `CopyFileEx`. Alternate /// NTFS streams are copied but only the size of the main stream is returned by -/// this function. On MacOS, this function corresponds to `copyfile` with -/// `COPYFILE_ALL`. +/// this function. On MacOS, this function corresponds to `fclonefileat` and +/// `fcopyfile`. /// Note that, this [may change in the future][changes]. /// /// [changes]: ../io/index.html#platform-specific-behavior diff --git a/src/libstd/sys/unix/fs.rs b/src/libstd/sys/unix/fs.rs index e653f6721f0..22c05a72a91 100644 --- a/src/libstd/sys/unix/fs.rs +++ b/src/libstd/sys/unix/fs.rs @@ -823,24 +823,28 @@ pub fn canonicalize(p: &Path) -> io::Result { Ok(PathBuf::from(OsString::from_vec(buf))) } -fn open_and_set_permissions( - from: &Path, - to: &Path, -) -> io::Result<(crate::fs::File, crate::fs::File, u64, crate::fs::Metadata)> { - use crate::fs::{File, OpenOptions}; - use crate::os::unix::fs::{OpenOptionsExt, PermissionsExt}; +fn open_from(from: &Path) -> io::Result<(crate::fs::File, crate::fs::Metadata)> { + use crate::fs::File; let reader = File::open(from)?; - let (perm, len) = { - let metadata = reader.metadata()?; - if !metadata.is_file() { - return Err(Error::new( - ErrorKind::InvalidInput, - "the source path is not an existing regular file", - )); - } - (metadata.permissions(), metadata.len()) - }; + let metadata = reader.metadata()?; + if !metadata.is_file() { + return Err(Error::new( + ErrorKind::InvalidInput, + "the source path is not an existing regular file", + )); + } + Ok((reader, metadata)) +} + +fn open_to_and_set_permissions( + to: &Path, + reader_metadata: crate::fs::Metadata, +) -> io::Result<(crate::fs::File, crate::fs::Metadata)> { + use crate::fs::OpenOptions; + use crate::os::unix::fs::{OpenOptionsExt, PermissionsExt}; + + let perm = reader_metadata.permissions(); let writer = OpenOptions::new() // create the file with the correct mode right away .mode(perm.mode()) @@ -855,7 +859,7 @@ fn open_and_set_permissions( // pipes/FIFOs or device nodes. writer.set_permissions(perm)?; } - Ok((reader, writer, len, writer_metadata)) + Ok((writer, writer_metadata)) } #[cfg(not(any(target_os = "linux", @@ -863,7 +867,8 @@ fn open_and_set_permissions( target_os = "macos", target_os = "ios")))] pub fn copy(from: &Path, to: &Path) -> io::Result { - let (mut reader, mut writer, _, _) = open_and_set_permissions(from, to)?; + let (mut reader, reader_metadata) = open_from(from)?; + let (mut writer, _) = open_to_and_set_permissions(to, reader_metadata)?; io::copy(&mut reader, &mut writer) } @@ -896,7 +901,9 @@ pub fn copy(from: &Path, to: &Path) -> io::Result { ) } - let (mut reader, mut writer, len, _) = open_and_set_permissions(from, to)?; + let (mut reader, reader_metadata) = open_from(from)?; + let len = reader_metadata.len(); + let (mut writer, _) = open_to_and_set_permissions(to, reader_metadata)?; let has_copy_file_range = HAS_COPY_FILE_RANGE.load(Ordering::Relaxed); let mut written = 0u64; @@ -955,6 +962,8 @@ pub fn copy(from: &Path, to: &Path) -> io::Result { #[cfg(any(target_os = "macos", target_os = "ios"))] pub fn copy(from: &Path, to: &Path) -> io::Result { + use crate::sync::atomic::{AtomicBool, Ordering}; + const COPYFILE_ACL: u32 = 1 << 0; const COPYFILE_STAT: u32 = 1 << 1; const COPYFILE_XATTR: u32 = 1 << 2; @@ -1000,7 +1009,48 @@ pub fn copy(from: &Path, to: &Path) -> io::Result { } } - let (reader, writer, _, writer_metadata) = open_and_set_permissions(from, to)?; + // MacOS prior to 10.12 don't support `fclonefileat` + // We store the availability in a global to avoid unnecessary syscalls + static HAS_FCLONEFILEAT: AtomicBool = AtomicBool::new(true); + syscall! { + fn fclonefileat( + srcfd: libc::c_int, + dst_dirfd: libc::c_int, + dst: *const libc::c_char, + flags: libc::c_int + ) -> libc::c_int + } + + let (reader, reader_metadata) = open_from(from)?; + + // Opportunistically attempt to create a copy-on-write clone of `from` + // using `fclonefileat`. + if HAS_FCLONEFILEAT.load(Ordering::Relaxed) { + let to = cstr(to)?; + let clonefile_result = cvt(unsafe { + fclonefileat( + reader.as_raw_fd(), + libc::AT_FDCWD, + to.as_ptr(), + 0, + ) + }); + match clonefile_result { + Ok(_) => return Ok(reader_metadata.len()), + Err(err) => match err.raw_os_error() { + // `fclonefileat` will fail on non-APFS volumes, if the + // destination already exists, or if the source and destination + // are on different devices. In all these cases `fcopyfile` + // should succeed. + Some(libc::ENOTSUP) | Some(libc::EEXIST) | Some(libc::EXDEV) => (), + Some(libc::ENOSYS) => HAS_FCLONEFILEAT.store(false, Ordering::Relaxed), + _ => return Err(err), + } + } + } + + // Fall back to using `fcopyfile` if `fclonefileat` does not succeed. + let (writer, writer_metadata) = open_to_and_set_permissions(to, reader_metadata)?; // We ensure that `FreeOnDrop` never contains a null pointer so it is // always safe to call `copyfile_state_free`