Skip to content

Commit df738ac

Browse files
committed
Make std::fs::copy attempt to create copy-on-write clones of files on MacOS.
1 parent ee1474a commit df738ac

File tree

2 files changed

+70
-21
lines changed

2 files changed

+70
-21
lines changed

src/libstd/fs.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1596,8 +1596,8 @@ pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> io::Result<()>
15961596
/// `O_CLOEXEC` is set for returned file descriptors.
15971597
/// On Windows, this function currently corresponds to `CopyFileEx`. Alternate
15981598
/// NTFS streams are copied but only the size of the main stream is returned by
1599-
/// this function. On MacOS, this function corresponds to `copyfile` with
1600-
/// `COPYFILE_ALL`.
1599+
/// this function. On MacOS, this function corresponds to `fclonefileat` and
1600+
/// `fcopyfile`.
16011601
/// Note that, this [may change in the future][changes].
16021602
///
16031603
/// [changes]: ../io/index.html#platform-specific-behavior

src/libstd/sys/unix/fs.rs

+68-19
Original file line numberDiff line numberDiff line change
@@ -808,24 +808,28 @@ pub fn canonicalize(p: &Path) -> io::Result<PathBuf> {
808808
Ok(PathBuf::from(OsString::from_vec(buf)))
809809
}
810810

811-
fn open_and_set_permissions(
812-
from: &Path,
811+
fn open_from(from: &Path) -> io::Result<(crate::fs::File, crate::fs::Metadata)> {
812+
use crate::fs::File;
813+
814+
let reader = File::open(from)?;
815+
let metadata = reader.metadata()?;
816+
if !metadata.is_file() {
817+
return Err(Error::new(
818+
ErrorKind::InvalidInput,
819+
"the source path is not an existing regular file",
820+
));
821+
}
822+
Ok((reader, metadata))
823+
}
824+
825+
fn open_to_and_set_permissions(
813826
to: &Path,
814-
) -> io::Result<(crate::fs::File, crate::fs::File, u64, crate::fs::Metadata)> {
815-
use crate::fs::{File, OpenOptions};
827+
reader_metadata: crate::fs::Metadata,
828+
) -> io::Result<(crate::fs::File, crate::fs::Metadata)> {
829+
use crate::fs::OpenOptions;
816830
use crate::os::unix::fs::{OpenOptionsExt, PermissionsExt};
817831

818-
let reader = File::open(from)?;
819-
let (perm, len) = {
820-
let metadata = reader.metadata()?;
821-
if !metadata.is_file() {
822-
return Err(Error::new(
823-
ErrorKind::InvalidInput,
824-
"the source path is not an existing regular file",
825-
));
826-
}
827-
(metadata.permissions(), metadata.len())
828-
};
832+
let perm = reader_metadata.permissions();
829833
let writer = OpenOptions::new()
830834
// create the file with the correct mode right away
831835
.mode(perm.mode())
@@ -840,15 +844,16 @@ fn open_and_set_permissions(
840844
// pipes/FIFOs or device nodes.
841845
writer.set_permissions(perm)?;
842846
}
843-
Ok((reader, writer, len, writer_metadata))
847+
Ok((writer, writer_metadata))
844848
}
845849

846850
#[cfg(not(any(target_os = "linux",
847851
target_os = "android",
848852
target_os = "macos",
849853
target_os = "ios")))]
850854
pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
851-
let (mut reader, mut writer, _, _) = open_and_set_permissions(from, to)?;
855+
let (mut reader, reader_metadata) = open_from(from)?;
856+
let (mut writer, _) = open_to_and_set_permissions(to, reader_metadata)?;
852857

853858
io::copy(&mut reader, &mut writer)
854859
}
@@ -881,7 +886,9 @@ pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
881886
)
882887
}
883888

884-
let (mut reader, mut writer, len, _) = open_and_set_permissions(from, to)?;
889+
let (mut reader, reader_metadata) = open_from(from)?;
890+
let len = reader_metadata.len();
891+
let (mut writer, _) = open_to_and_set_permissions(to, reader_metadata)?;
885892

886893
let has_copy_file_range = HAS_COPY_FILE_RANGE.load(Ordering::Relaxed);
887894
let mut written = 0u64;
@@ -940,6 +947,8 @@ pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
940947

941948
#[cfg(any(target_os = "macos", target_os = "ios"))]
942949
pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
950+
use crate::sync::atomic::{AtomicBool, Ordering};
951+
943952
const COPYFILE_ACL: u32 = 1 << 0;
944953
const COPYFILE_STAT: u32 = 1 << 1;
945954
const COPYFILE_XATTR: u32 = 1 << 2;
@@ -985,7 +994,47 @@ pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
985994
}
986995
}
987996

988-
let (reader, writer, _, writer_metadata) = open_and_set_permissions(from, to)?;
997+
// MacOS prior to 10.12 don't support `fclonefileat`
998+
// We store the availability in a global to avoid unnecessary syscalls
999+
static HAS_FCLONEFILEAT: AtomicBool = AtomicBool::new(true);
1000+
syscall! {
1001+
fn fclonefileat(
1002+
srcfd: libc::c_int,
1003+
dst_dirfd: libc::c_int,
1004+
dst: *const libc::c_char,
1005+
flags: libc::c_int
1006+
) -> libc::c_int
1007+
}
1008+
1009+
let (reader, reader_metadata) = open_from(from)?;
1010+
1011+
// Opportunistically attempt to create a copy-on-write clone of `from`
1012+
// using `fclonefileat`.
1013+
if HAS_FCLONEFILEAT.load(Ordering::Relaxed) {
1014+
let clonefile_result = cvt(unsafe {
1015+
fclonefileat(
1016+
reader.as_raw_fd(),
1017+
libc::AT_FDCWD,
1018+
cstr(to)?.as_ptr(),
1019+
0,
1020+
)
1021+
});
1022+
match clonefile_result {
1023+
Ok(_) => return Ok(reader_metadata.len()),
1024+
Err(err) => match err.raw_os_error() {
1025+
// `fclonefileat` will fail on non-APFS volumes, if the
1026+
// destination already exists, or if the source and destination
1027+
// are on different devices. In all these cases `fcopyfile`
1028+
// should succeed.
1029+
Some(libc::ENOTSUP) | Some(libc::EEXIST) | Some(libc::EXDEV) => (),
1030+
Some(libc::ENOSYS) => HAS_FCLONEFILEAT.store(false, Ordering::Relaxed),
1031+
_ => return Err(err),
1032+
}
1033+
}
1034+
}
1035+
1036+
// Fall back to using `fcopyfile` if `fclonefileat` does not succeed.
1037+
let (writer, writer_metadata) = open_to_and_set_permissions(to, reader_metadata)?;
9891038

9901039
// We ensure that `FreeOnDrop` never contains a null pointer so it is
9911040
// always safe to call `copyfile_state_free`

0 commit comments

Comments
 (0)