Skip to content

Use CopyFileEx for fs::copy on Windows #26751

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 10, 2015
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 27 additions & 15 deletions src/libstd/fs.rs
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ use core::prelude::*;

use fmt;
use ffi::OsString;
use io::{self, Error, ErrorKind, SeekFrom, Seek, Read, Write};
use io::{self, SeekFrom, Seek, Read, Write};
use path::{Path, PathBuf};
use sys::fs as fs_imp;
use sys_common::{AsInnerMut, FromInner, AsInner};
@@ -858,20 +858,7 @@ pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> io::Result<()>
/// ```
#[stable(feature = "rust1", since = "1.0.0")]
pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> io::Result<u64> {
let from = from.as_ref();
let to = to.as_ref();
if !from.is_file() {
return Err(Error::new(ErrorKind::InvalidInput,
"the source path is not an existing file"))
}

let mut reader = try!(File::open(from));
let mut writer = try!(File::create(to));
let perm = try!(reader.metadata()).permissions();

let ret = try!(io::copy(&mut reader, &mut writer));
try!(set_permissions(to, perm));
Ok(ret)
fs_imp::copy(from.as_ref(), to.as_ref())
}

/// Creates a new hard link on the filesystem.
@@ -1745,6 +1732,19 @@ mod tests {
}
}

#[test]
fn copy_src_does_not_exist() {
let tmpdir = tmpdir();
let from = Path2::new("test/nonexistent-bogus-path");
let to = tmpdir.join("out.txt");
check!(check!(File::create(&to)).write(b"hello"));
assert!(fs::copy(&from, &to).is_err());
assert!(!from.exists());
let mut v = Vec::new();
check!(check!(File::open(&to)).read_to_end(&mut v));
assert_eq!(v, b"hello");
}

#[test]
fn copy_file_ok() {
let tmpdir = tmpdir();
@@ -1814,6 +1814,18 @@ mod tests {
check!(fs::set_permissions(&out, attr.permissions()));
}

#[cfg(windows)]
#[test]
fn copy_file_preserves_streams() {
Copy link
Member

Choose a reason for hiding this comment

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

This seems surprising! Could you elaborate on what's going on here? E.g. how come this test is only enabled for Windows?

Copy link
Contributor

Choose a reason for hiding this comment

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

In windows NTFS), every file name maps to multiple actual files called streams. By default, reading/writing/deleting operates on the "anonymous" stream but you can operate on other streams. Alternate streams are kind of like extended attributes but more flexible.

https://technet.microsoft.com/en-us/sysinternals/bb897440.aspx

Copy link
Member Author

Choose a reason for hiding this comment

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

I made the test specific to Windows since it depends on file streams which are specific to NTFS, and Windows is the only OS we support that consistently uses NTFS. Also, it depends on the implementation properly copying over all the file streams, which the manual implementation does not do, so even if another OS did support NTFS file streams, this test would fail on such an OS at the moment.

Copy link
Member Author

Choose a reason for hiding this comment

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

To elaborate further on what's going on, I'm creating the file in.txt but instead of writing to the default data stream, I'm instead writing to the bunny stream.
If I copy the file to a new location it brings all the streams with it, and the number of bytes copied (what I assume the return value from fs::copy means) is the same as the number of bytes I wrote to the bunny stream since its the only stream with data.
When getting the size of the file, I'm getting that information on the default data stream, which is empty so its size is 0. If I wanted information on the other streams I'd have to explicitly specify them by name.

Copy link
Member

Choose a reason for hiding this comment

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

Cool, thanks for the explanations!

let tmp = tmpdir();
check!(check!(File::create(tmp.join("in.txt:bunny"))).write("carrot".as_bytes()));
assert_eq!(check!(fs::copy(tmp.join("in.txt"), tmp.join("out.txt"))), 6);
assert_eq!(check!(tmp.join("out.txt").metadata()).len(), 0);
let mut v = Vec::new();
check!(check!(File::open(tmp.join("out.txt:bunny"))).read_to_end(&mut v));
assert_eq!(v, b"carrot".to_vec());
}

#[cfg(not(windows))] // FIXME(#10264) operation not permitted?
#[test]
fn symlinks_work() {
18 changes: 17 additions & 1 deletion src/libstd/sys/unix/fs.rs
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ use os::unix::prelude::*;

use ffi::{CString, CStr, OsString, OsStr};
use fmt;
use io::{self, Error, SeekFrom};
use io::{self, Error, ErrorKind, SeekFrom};
use libc::{self, c_int, size_t, off_t, c_char, mode_t};
use mem;
use path::{Path, PathBuf};
@@ -516,3 +516,19 @@ pub fn canonicalize(p: &Path) -> io::Result<PathBuf> {
buf.truncate(p);
Ok(PathBuf::from(OsString::from_vec(buf)))
}

pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
use fs::{File, PathExt, set_permissions};
if !from.is_file() {
return Err(Error::new(ErrorKind::InvalidInput,
"the source path is not an existing file"))
}

let mut reader = try!(File::open(from));
let mut writer = try!(File::create(to));
let perm = try!(reader.metadata()).permissions();

let ret = try!(io::copy(&mut reader, &mut writer));
try!(set_permissions(to, perm));
Ok(ret)
}
24 changes: 24 additions & 0 deletions src/libstd/sys/windows/c.rs
Original file line number Diff line number Diff line change
@@ -66,6 +66,11 @@ pub const STD_ERROR_HANDLE: libc::DWORD = -12i32 as libc::DWORD;

pub const HANDLE_FLAG_INHERIT: libc::DWORD = 0x00000001;

pub const PROGRESS_CONTINUE: libc::DWORD = 0;
pub const PROGRESS_CANCEL: libc::DWORD = 1;
pub const PROGRESS_STOP: libc::DWORD = 2;
pub const PROGRESS_QUIET: libc::DWORD = 3;

#[repr(C)]
#[cfg(target_arch = "x86")]
pub struct WSADATA {
@@ -249,6 +254,19 @@ pub type PCONDITION_VARIABLE = *mut CONDITION_VARIABLE;
pub type PSRWLOCK = *mut SRWLOCK;
pub type ULONG = c_ulong;
pub type ULONG_PTR = c_ulong;
pub type LPBOOL = *mut BOOL;

pub type LPPROGRESS_ROUTINE = ::option::Option<unsafe extern "system" fn(
TotalFileSize: libc::LARGE_INTEGER,
TotalBytesTransferred: libc::LARGE_INTEGER,
StreamSize: libc::LARGE_INTEGER,
StreamBytesTransferred: libc::LARGE_INTEGER,
dwStreamNumber: DWORD,
dwCallbackReason: DWORD,
hSourceFile: HANDLE,
hDestinationFile: HANDLE,
lpData: LPVOID,
) -> DWORD>;

#[repr(C)]
pub struct CONDITION_VARIABLE { pub ptr: LPVOID }
@@ -413,6 +431,12 @@ extern "system" {
pub fn SetHandleInformation(hObject: libc::HANDLE,
dwMask: libc::DWORD,
dwFlags: libc::DWORD) -> libc::BOOL;
pub fn CopyFileExW(lpExistingFileName: libc::LPCWSTR,
lpNewFileName: libc::LPCWSTR,
lpProgressRoutine: LPPROGRESS_ROUTINE,
lpData: libc::LPVOID,
pbCancel: LPBOOL,
dwCopyFlags: libc::DWORD) -> libc::BOOL;
}

// Functions that aren't available on Windows XP, but we still use them and just
25 changes: 25 additions & 0 deletions src/libstd/sys/windows/fs.rs
Original file line number Diff line number Diff line change
@@ -575,3 +575,28 @@ pub fn canonicalize(p: &Path) -> io::Result<PathBuf> {
PathBuf::from(OsString::from_wide(buf))
})
}

pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
unsafe extern "system" fn callback(
_TotalFileSize: libc::LARGE_INTEGER,
TotalBytesTransferred: libc::LARGE_INTEGER,
_StreamSize: libc::LARGE_INTEGER,
_StreamBytesTransferred: libc::LARGE_INTEGER,
_dwStreamNumber: libc::DWORD,
_dwCallbackReason: libc::DWORD,
_hSourceFile: HANDLE,
_hDestinationFile: HANDLE,
lpData: libc::LPVOID,
) -> libc::DWORD {
*(lpData as *mut i64) = TotalBytesTransferred;
c::PROGRESS_CONTINUE
}
let pfrom = to_utf16(from);
let pto = to_utf16(to);
let mut size = 0i64;
try!(cvt(unsafe {
c::CopyFileExW(pfrom.as_ptr(), pto.as_ptr(), Some(callback),
&mut size as *mut _ as *mut _, ptr::null_mut(), 0)
}));
Ok(size as u64)
}