Skip to content
88 changes: 88 additions & 0 deletions src/libstd/sys/unix/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,7 @@ pub fn canonicalize(p: &Path) -> io::Result<PathBuf> {
Ok(PathBuf::from(OsString::from_vec(buf)))
}

#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
use fs::{File, set_permissions};
if !from.is_file() {
Expand All @@ -776,3 +777,90 @@ pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
set_permissions(to, perm)?;
Ok(ret)
}

#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
use cmp;
use fs::{File, set_permissions};
use sync::atomic::{AtomicBool, Ordering};

// Kernel prior to 4.5 don't have copy_file_range
// We store the availability in a global to avoid unneccessary syscalls
static HAS_COPY_FILE_RANGE: AtomicBool = AtomicBool::new(true);

unsafe fn copy_file_range(
fd_in: libc::c_int,
off_in: *mut libc::loff_t,
fd_out: libc::c_int,
off_out: *mut libc::loff_t,
len: libc::size_t,
flags: libc::c_uint,
) -> libc::c_long {
libc::syscall(
libc::SYS_copy_file_range,
fd_in,
off_in,
fd_out,
off_out,
len,
flags,
)
}

if !from.is_file() {
return Err(Error::new(ErrorKind::InvalidInput,
"the source path is not an existing regular file"))
}

let mut reader = File::open(from)?;
let mut writer = File::create(to)?;
let (perm, len) = {
let metadata = reader.metadata()?;
(metadata.permissions(), metadata.size())
};

let has_copy_file_range = HAS_COPY_FILE_RANGE.load(Ordering::Relaxed);
let mut written = 0u64;
while written < len {
let copy_result = if has_copy_file_range {
let bytes_to_copy = cmp::min(len - written, usize::max_value() as u64) as usize;
let copy_result = unsafe {
// We actually don't have to adjust the offsets,
// because copy_file_range adjusts the file offset automatically
cvt(copy_file_range(reader.as_raw_fd(),
ptr::null_mut(),
writer.as_raw_fd(),
ptr::null_mut(),
bytes_to_copy,
0)
)
};
if let Err(ref copy_err) = copy_result {
if let Some(libc::ENOSYS) = copy_err.raw_os_error() {
HAS_COPY_FILE_RANGE.store(false, Ordering::Relaxed);
Copy link
Contributor

@meven meven Jun 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't has_copy_file_range be updated to false as well ?
For the first file, after the first chunk of the file has been copied, if copy_file_range was not available, next iterations of the while loop will try again to use copy_file_range and fail continuously, we could avoid it, making mut has_copy_file_range and here has_copy_file_range = false;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. If copy_result is Err the loop will always terminate in the current iteration.

}
}
copy_result
} else {
Err(io::Error::from_raw_os_error(libc::ENOSYS))
};
match copy_result {
Ok(ret) => written += ret as u64,
Err(err) => {
match err.raw_os_error() {
Some(os_err) if os_err == libc::ENOSYS || os_err == libc::EXDEV => {
// Either kernel is too old or the files are not mounted on the same fs.
// Try again with fallback method
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also throw an assert in here that written is 0? I don't think it's possible for it to not be 0 but the return value at least I believe will be invalid if it's nonzero

assert_eq!(written, 0);
let ret = io::copy(&mut reader, &mut writer)?;
set_permissions(to, perm)?;
return Ok(ret)
},
_ => return Err(err),
}
}
}
}
set_permissions(to, perm)?;
Ok(written)
}