Skip to content

Make fs::rename on windows obey Unix rename semantics. #1583 #17203

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
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
56 changes: 54 additions & 2 deletions src/libstd/io/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,9 +421,34 @@ fn from_rtio(s: rtio::FileStat) -> FileStat {
/// # Error
///
/// This function will return an error if the provided `from` doesn't exist, if
/// the process lacks permissions to view the contents, or if some other
/// intermittent I/O error occurs.
/// the process lacks permissions to view the contents, if `to` both exists *and*
//// is non-empty, or if some other intermittent I/O error occurs.
Copy link
Contributor

Choose a reason for hiding this comment

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

A / too much.

pub fn rename(from: &Path, to: &Path) -> IoResult<()> {

if cfg!(windows) {
// POSIX `rename` will overwrite a directory only if it's empty
// and those are Rust's semantics as well. Here emulating
// that on Windows. This workaround is here so that both libnative and
// libgreen benefit, though it's not clear to me this is the right abstraction
// layer for it.
match stat(to) {
Ok(stat) if stat.kind == io::TypeDirectory => {
// FIXME: Surely there is a faster solution here than reading the entire directory
let files = try!(readdir(to));
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of checking whether ther directory is non-empty, I think you can just rmdir it – this works only on empty directories according to the docs. This would remove the unnecessary allocation.

if files.is_empty() {
// Delete the directory and proceed
try!(rmdir(to));
} else {
// Emulate the Unix directory not empty error
return Err(standard_error(io::DirectoryNotEmpty))
}
}
Ok(_) => (/* not a directory. fine */),
Err(ref e) if e.kind == io::FileNotFound => (/* destination doesn't exist. fine */),
Err(e) => return Err(e.clone())
}
}
Copy link
Member

Choose a reason for hiding this comment

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

In general I try to move these workarounds to only happen in the error case of fs_rename, that way the "fast path" avoids the extra syscall to stat/readdir/etc.


let err = LocalIo::maybe_raise(|io| {
io.fs_rename(&from.to_c_str(), &to.to_c_str())
}).map_err(IoError::from_rtio_error);
Expand Down Expand Up @@ -1633,4 +1658,31 @@ mod test {
check!(chmod(&path, io::UserRead));
check!(unlink(&path));
})

iotest!(fn rename_directory_overwrite_empty() {
let tmpdir = tmpdir();
let dir1 = tmpdir.join("dir1");
let dir2 = tmpdir.join("dir2");
check!(mkdir(&dir1, UserRWX));
check!(mkdir(&dir2, UserRWX));
check!(rename(&dir1, &dir2));
assert!(!dir1.exists());
assert!(dir2.exists());
})

iotest!(fn rename_directory_no_overwrite_non_empty() {
let tmpdir = tmpdir();
let dir1 = tmpdir.join("dir1");
let dir2 = tmpdir.join("dir2");
check!(mkdir(&dir1, UserRWX));
check!(mkdir(&dir2, UserRWX));
let file = dir2.join("file.txt");
check!(File::create(&file));
match rename(&dir1, &dir2) {
Ok(_) => fail!("rename succeeded"),
Err(e) => assert!(e.kind == io::DirectoryNotEmpty)
}
assert!(dir1.exists());
assert!(dir2.exists());
})
}
4 changes: 4 additions & 0 deletions src/libstd/io/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ impl IoError {
"file descriptor is not a TTY"),
libc::ETIMEDOUT => (TimedOut, "operation timed out"),
libc::ECANCELED => (TimedOut, "operation aborted"),
libc::ENOTEMPTY => (DirectoryNotEmpty, "directory is not empty"),

// These two constants can have the same value on some systems,
// but different values on others, so we can't use a match
Expand Down Expand Up @@ -478,6 +479,8 @@ pub enum IoErrorKind {
InvalidInput,
/// The I/O operation's timeout expired, causing it to be canceled.
TimedOut,
/// The destination directory in `rename` is not empty.
DirectoryNotEmpty,
/// This write operation failed to write all of its data.
///
/// Normally the write() method on a Writer guarantees that all of its data
Expand Down Expand Up @@ -1662,6 +1665,7 @@ pub fn standard_error(kind: IoErrorKind) -> IoError {
TimedOut => "operation timed out",
ShortWrite(..) => "short write",
NoProgress => "no progress",
DirectoryNotEmpty => "directory is not empty"
};
IoError {
kind: kind,
Expand Down