Skip to content

Commit c177bc9

Browse files
authored
Rollup merge of rust-lang#58901 - ebarnard:just-copying, r=sfackler
Change `std::fs::copy` to use `copyfile` on MacOS and iOS `copyfile` on MacOS is similar to `CopyFileEx` on Windows. It supports copying resource forks, extended attributes, and file ACLs, none of which are copied by the current generic unix implementation. The API is available from MacOS 10.7 and iOS 4.3 (and possibly earlier but I haven't checked). Closes rust-lang#58895.
2 parents 789aca0 + 124ab2a commit c177bc9

File tree

2 files changed

+108
-2
lines changed

2 files changed

+108
-2
lines changed

src/libstd/fs.rs

+22-1
Original file line numberDiff line numberDiff line change
@@ -1581,7 +1581,8 @@ pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> io::Result<()>
15811581
/// `O_CLOEXEC` is set for returned file descriptors.
15821582
/// On Windows, this function currently corresponds to `CopyFileEx`. Alternate
15831583
/// NTFS streams are copied but only the size of the main stream is returned by
1584-
/// this function.
1584+
/// this function. On MacOS, this function corresponds to `copyfile` with
1585+
/// `COPYFILE_ALL`.
15851586
/// Note that, this [may change in the future][changes].
15861587
///
15871588
/// [changes]: ../io/index.html#platform-specific-behavior
@@ -2836,6 +2837,26 @@ mod tests {
28362837
assert_eq!(check!(out_path.metadata()).len(), copied_len);
28372838
}
28382839

2840+
#[test]
2841+
fn copy_file_follows_dst_symlink() {
2842+
let tmp = tmpdir();
2843+
if !got_symlink_permission(&tmp) { return };
2844+
2845+
let in_path = tmp.join("in.txt");
2846+
let out_path = tmp.join("out.txt");
2847+
let out_path_symlink = tmp.join("out_symlink.txt");
2848+
2849+
check!(fs::write(&in_path, "foo"));
2850+
check!(fs::write(&out_path, "bar"));
2851+
check!(symlink_file(&out_path, &out_path_symlink));
2852+
2853+
check!(fs::copy(&in_path, &out_path_symlink));
2854+
2855+
assert!(check!(out_path_symlink.symlink_metadata()).file_type().is_symlink());
2856+
assert_eq!(check!(fs::read(&out_path_symlink)), b"foo".to_vec());
2857+
assert_eq!(check!(fs::read(&out_path)), b"foo".to_vec());
2858+
}
2859+
28392860
#[test]
28402861
fn symlinks_work() {
28412862
let tmpdir = tmpdir();

src/libstd/sys/unix/fs.rs

+86-1
Original file line numberDiff line numberDiff line change
@@ -827,7 +827,10 @@ pub fn canonicalize(p: &Path) -> io::Result<PathBuf> {
827827
Ok(PathBuf::from(OsString::from_vec(buf)))
828828
}
829829

830-
#[cfg(not(any(target_os = "linux", target_os = "android")))]
830+
#[cfg(not(any(target_os = "linux",
831+
target_os = "android",
832+
target_os = "macos",
833+
target_os = "ios")))]
831834
pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
832835
use crate::fs::File;
833836
if !from.is_file() {
@@ -954,3 +957,85 @@ pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
954957
}
955958
Ok(written)
956959
}
960+
961+
#[cfg(any(target_os = "macos", target_os = "ios"))]
962+
pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
963+
const COPYFILE_ACL: u32 = 1 << 0;
964+
const COPYFILE_STAT: u32 = 1 << 1;
965+
const COPYFILE_XATTR: u32 = 1 << 2;
966+
const COPYFILE_DATA: u32 = 1 << 3;
967+
968+
const COPYFILE_SECURITY: u32 = COPYFILE_STAT | COPYFILE_ACL;
969+
const COPYFILE_METADATA: u32 = COPYFILE_SECURITY | COPYFILE_XATTR;
970+
const COPYFILE_ALL: u32 = COPYFILE_METADATA | COPYFILE_DATA;
971+
972+
const COPYFILE_STATE_COPIED: u32 = 8;
973+
974+
#[allow(non_camel_case_types)]
975+
type copyfile_state_t = *mut libc::c_void;
976+
#[allow(non_camel_case_types)]
977+
type copyfile_flags_t = u32;
978+
979+
extern "C" {
980+
fn copyfile(
981+
from: *const libc::c_char,
982+
to: *const libc::c_char,
983+
state: copyfile_state_t,
984+
flags: copyfile_flags_t,
985+
) -> libc::c_int;
986+
fn copyfile_state_alloc() -> copyfile_state_t;
987+
fn copyfile_state_free(state: copyfile_state_t) -> libc::c_int;
988+
fn copyfile_state_get(
989+
state: copyfile_state_t,
990+
flag: u32,
991+
dst: *mut libc::c_void,
992+
) -> libc::c_int;
993+
}
994+
995+
struct FreeOnDrop(copyfile_state_t);
996+
impl Drop for FreeOnDrop {
997+
fn drop(&mut self) {
998+
// The code below ensures that `FreeOnDrop` is never a null pointer
999+
unsafe {
1000+
// `copyfile_state_free` returns -1 if the `to` or `from` files
1001+
// cannot be closed. However, this is not considerd this an
1002+
// error.
1003+
copyfile_state_free(self.0);
1004+
}
1005+
}
1006+
}
1007+
1008+
if !from.is_file() {
1009+
return Err(Error::new(ErrorKind::InvalidInput,
1010+
"the source path is not an existing regular file"))
1011+
}
1012+
1013+
// We ensure that `FreeOnDrop` never contains a null pointer so it is
1014+
// always safe to call `copyfile_state_free`
1015+
let state = unsafe {
1016+
let state = copyfile_state_alloc();
1017+
if state.is_null() {
1018+
return Err(crate::io::Error::last_os_error());
1019+
}
1020+
FreeOnDrop(state)
1021+
};
1022+
1023+
cvt(unsafe {
1024+
copyfile(
1025+
cstr(from)?.as_ptr(),
1026+
cstr(to)?.as_ptr(),
1027+
state.0,
1028+
COPYFILE_ALL,
1029+
)
1030+
})?;
1031+
1032+
let mut bytes_copied: libc::off_t = 0;
1033+
cvt(unsafe {
1034+
copyfile_state_get(
1035+
state.0,
1036+
COPYFILE_STATE_COPIED,
1037+
&mut bytes_copied as *mut libc::off_t as *mut libc::c_void,
1038+
)
1039+
})?;
1040+
Ok(bytes_copied as u64)
1041+
}

0 commit comments

Comments
 (0)