]> git.lizzy.rs Git - rust.git/commitdiff
Make `std::fs::copy` attempt to create copy-on-write clones of files on MacOS.
authorEdward Barnard <eabarnard@gmail.com>
Thu, 11 Apr 2019 15:02:13 +0000 (16:02 +0100)
committerEdward Barnard <eabarnard@gmail.com>
Thu, 2 May 2019 08:41:37 +0000 (09:41 +0100)
src/libstd/fs.rs
src/libstd/sys/unix/fs.rs

index eaf5d619f5454b1a48a9728cc41b07f5a4072916..991b45fd4a2ec2caf03b10b4d49237967226c7f2 100644 (file)
@@ -1615,8 +1615,8 @@ pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(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
index e653f6721f06257fea23b7f05a238f196e57b612..22c05a72a91c04289865bc57a72ef215d94a605c 100644 (file)
@@ -823,24 +823,28 @@ pub fn canonicalize(p: &Path) -> io::Result<PathBuf> {
     Ok(PathBuf::from(OsString::from_vec(buf)))
 }
 
-fn open_and_set_permissions(
-    from: &Path,
+fn open_from(from: &Path) -> io::Result<(crate::fs::File, crate::fs::Metadata)> {
+    use crate::fs::File;
+
+    let reader = File::open(from)?;
+    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,
-) -> io::Result<(crate::fs::File, crate::fs::File, u64, crate::fs::Metadata)> {
-    use crate::fs::{File, OpenOptions};
+    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 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 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<u64> {
-    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 @@ unsafe fn copy_file_range(
         )
     }
 
-    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 @@ unsafe fn copy_file_range(
 
 #[cfg(any(target_os = "macos", target_os = "ios"))]
 pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
+    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 @@ fn drop(&mut self) {
         }
     }
 
-    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`