Skip to content
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

Fix fs::remove_dir_all #31944

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 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
35 changes: 34 additions & 1 deletion src/libstd/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1426,7 +1426,8 @@ impl Iterator for WalkDir {
/// # Platform-specific behavior
///
/// This function currently corresponds to the `chmod` function on Unix
/// and the `SetFileAttributes` function on Windows.
/// and the `CreateFile`, `GetFileInformationByHandle` and
/// `SetFileInformationByHandle` function on Windows.
/// Note that, this [may change in the future][changes].
/// [changes]: ../io/index.html#platform-specific-behavior
///
Expand Down Expand Up @@ -1980,6 +1981,38 @@ mod tests {
}
}

#[test]
#[cfg(windows)] // only tricky on Windows
fn recursive_rmdir_tricky() {
use os::windows::fs::OpenOptionsExt;
let tmpdir = tmpdir();
let dir = tmpdir.join("dir");
check!(fs::create_dir(&dir));
// filenames that can only be accessed with `/??/`-paths
let fullpath = check!(dir.canonicalize());
check!(File::create(fullpath.join("morse .. .")));
check!(File::create(fullpath.join("con")));
// read-only file
{
let mut opts = fs::OpenOptions::new();
opts.write(true);
opts.create(true);
opts.attributes(0x1); // FILE_ATTRIBUTE_READONLY
Copy link
Member

Choose a reason for hiding this comment

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

Could this reach into sys::c to get this constant out?

Copy link
Member

Choose a reason for hiding this comment

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

Also if this is the only platform-specific part of this test, couldn't this be a platform-agnostic test by using set_permissions?

let _ = check!(opts.open(dir.join("readonly")));
}
// hardlink outside this directory should not lose its read-only flag
check!(fs::hard_link(dir.join("readonly"), tmpdir.join("canary_ro")));
// open file
let mut opts = fs::OpenOptions::new();
let mut file_open = check!(opts.write(true).create(true)
.open(dir.join("remains_open")));

check!(fs::remove_dir_all(&dir));
assert!(check!(tmpdir.join("canary_ro").metadata())
.permissions().readonly());
check!(file_open.write("something".as_bytes()));
}

#[test]
fn unicode_path_is_dir() {
assert!(Path::new(".").is_dir());
Expand Down
27 changes: 25 additions & 2 deletions src/libstd/sys/windows/c.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ pub const TRUNCATE_EXISTING: DWORD = 5;
pub const FILE_WRITE_DATA: DWORD = 0x00000002;
pub const FILE_APPEND_DATA: DWORD = 0x00000004;
pub const FILE_WRITE_EA: DWORD = 0x00000010;
pub const FILE_READ_ATTRIBUTES: DWORD = 0x00000080;
pub const FILE_WRITE_ATTRIBUTES: DWORD = 0x00000100;
pub const DELETE: DWORD = 0x00010000;
pub const READ_CONTROL: DWORD = 0x00020000;
pub const SYNCHRONIZE: DWORD = 0x00100000;
pub const GENERIC_READ: DWORD = 0x80000000;
Expand All @@ -112,6 +114,7 @@ pub const FILE_GENERIC_WRITE: DWORD = STANDARD_RIGHTS_WRITE | FILE_WRITE_DATA |

pub const FILE_FLAG_OPEN_REPARSE_POINT: DWORD = 0x00200000;
pub const FILE_FLAG_BACKUP_SEMANTICS: DWORD = 0x02000000;
pub const FILE_FLAG_DELETE_ON_CLOSE: DWORD = 0x04000000;
pub const SECURITY_SQOS_PRESENT: DWORD = 0x00100000;

#[repr(C)]
Expand Down Expand Up @@ -364,6 +367,28 @@ pub enum FILE_INFO_BY_HANDLE_CLASS {
MaximumFileInfoByHandlesClass
}

#[repr(C)]
pub struct FILE_BASIC_INFO {
pub CreationTime: LARGE_INTEGER,
pub LastAccessTime: LARGE_INTEGER,
pub LastWriteTime: LARGE_INTEGER,
pub ChangeTime: LARGE_INTEGER,
pub FileAttributes: DWORD,
}

#[repr(C)]
pub struct FILE_RENAME_INFO {
pub ReplaceIfExists: BOOL, // for true use -1, not TRUE
pub RootDirectory: HANDLE, // NULL, or obtained with NtOpenDirectoryObject
pub FileNameLength: DWORD,
pub FileName: [WCHAR; 0],
}

#[repr(C)]
pub struct FILE_DISPOSITION_INFO {
pub DeleteFile: BOOL, // for true use -1, not TRUE
Copy link
Member

Choose a reason for hiding this comment

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

Why does this need to be -1? I don't see anything on MSDN for why this can't be TRUE.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This struct is the same as FILE_DISPOSITION_INFORMATION which uses a BOOLEAN. But TRUE just doesn't work, it gives an error. The documentation for this fuction is not great, this one is better: https://msdn.microsoft.com/nl-nl/library/windows/hardware/ff567096(v=vs.85).aspx

Copy link
Member

Choose a reason for hiding this comment

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

Set this member to TRUE to delete the file when it is closed.

Nowhere do I see any mention of -1. What error do you get when you use TRUE instead of -1?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Something like: the file name, directory name or volume lable syntax is incorrect (I am on mobile, can't test now). But it took me hours to get working because of this bug. Only afterwards I found out this function is just a very thin wrapper around an NT internal function that takes a BOOLEAN instead of a BOOL, explaining the problem.

Copy link
Member

Choose a reason for hiding this comment

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

Can you move this comment to where it's called down in remove(&self) and put some of these comments there as well? Sounds like it'll be useful info for the next reader!

}

#[repr(C)]
pub struct FILE_END_OF_FILE_INFO {
pub EndOfFile: LARGE_INTEGER,
Expand Down Expand Up @@ -855,8 +880,6 @@ extern "system" {
pub fn GetConsoleMode(hConsoleHandle: HANDLE,
lpMode: LPDWORD) -> BOOL;
pub fn RemoveDirectoryW(lpPathName: LPCWSTR) -> BOOL;
pub fn SetFileAttributesW(lpFileName: LPCWSTR,
dwFileAttributes: DWORD) -> BOOL;
pub fn GetFileInformationByHandle(hFile: HANDLE,
lpFileInformation: LPBY_HANDLE_FILE_INFORMATION)
-> BOOL;
Expand Down
201 changes: 171 additions & 30 deletions src/libstd/sys/windows/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use sys::handle::Handle;
use sys::time::SystemTime;
use sys::{c, cvt};
use sys_common::FromInner;
use vec::Vec;

use super::to_u16s;

Expand Down Expand Up @@ -77,8 +78,8 @@ pub struct OpenOptions {
security_attributes: usize, // FIXME: should be a reference
}

#[derive(Clone, PartialEq, Eq, Debug)]
pub struct FilePermissions { attrs: c::DWORD }
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct FilePermissions { readonly: bool }

pub struct DirBuilder;

Expand Down Expand Up @@ -333,6 +334,90 @@ impl File {
Ok(newpos as u64)
}

pub fn remove(&self) -> io::Result<()> {
let mut info = c::FILE_DISPOSITION_INFO {
DeleteFile: -1,
};
let size = mem::size_of_val(&info);
try!(cvt(unsafe {
c::SetFileInformationByHandle(self.handle.raw(),
c::FileDispositionInfo,
&mut info as *mut _ as *mut _,
size as c::DWORD)
}));
Ok(())
}

pub fn rename(&self, new: &Path, replace: bool) -> io::Result<()> {
// &self must be opened with DELETE permission
use iter;
#[cfg(target_arch = "x86")]
const STRUCT_SIZE: usize = 12;
#[cfg(target_arch = "x86_64")]
const STRUCT_SIZE: usize = 20;
Copy link
Member

Choose a reason for hiding this comment

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

Could this be done with mem::size_of::<FILE_RENAME_INFO>?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not really, as this is the size of the struct without padding at the end.
I will add a comment.

Copy link
Member

Choose a reason for hiding this comment

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

Hm, perhaps this could be something like offset_of!(relevant_field, relevant_struct)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this an existing macro? I can't find it...

Copy link
Member

Choose a reason for hiding this comment

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

Ah yeah sorry that was intended as basically "we should calculate the offset of a field and use that instead". The macro itself doesn't exist in-tree yet, but here's an implementation of it:

https://github.com/alexcrichton/ctest/blob/50ac771acb7bb45cf0c182a5a9c8188a15c89efc/src/lib.rs#L373-L377


// FIXME: check for internal NULs in 'new'
Copy link
Member

Choose a reason for hiding this comment

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

This to_u16s helper may be useful here, although it may need some tweaks if you want to avoid the extra allocation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have taken a look at to_u16s before, and would like to change it a bit. I am not sure how many allocations it does now when building the Vec, but only one should be enough. It is simple to precompute how many u16's an OsStr converts to. Together with optionally reserving some space for a struct, it should help here and with symlink_junction.
But I would like to leave this for an other pr, this one is getting pretty big for me.

Copy link
Member

Choose a reason for hiding this comment

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

Ok, sounds good to me. Right now this is only called for existing files so I don't think it's an error case we'll run into anyway.

let mut data: Vec<u16> = iter::repeat(0u16).take(STRUCT_SIZE/2)
.chain(new.as_os_str().encode_wide())
.collect();
data.push(0);
let size = data.len() * 2;

unsafe {
// Thanks to alignment guarantees on Windows this works
// (8 for 32-bit and 16 for 64-bit)
let mut info = data.as_mut_ptr() as *mut c::FILE_RENAME_INFO;
(*info).ReplaceIfExists = if replace { -1 } else { c::FALSE };
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 add some docs here as well for -1 vs TRUE?

(*info).RootDirectory = ptr::null_mut();
(*info).FileNameLength = (size - STRUCT_SIZE) as c::DWORD;
try!(cvt(c::SetFileInformationByHandle(self.handle().raw(),
c::FileRenameInfo,
data.as_mut_ptr() as *mut _ as *mut _,
size as c::DWORD)));
Ok(())
}
}

pub fn set_attributes(&self, attr: c::DWORD) -> io::Result<()> {
let mut info = c::FILE_BASIC_INFO {
CreationTime: 0, // do not change
LastAccessTime: 0, // do not change
LastWriteTime: 0, // do not change
ChangeTime: 0, // do not change
FileAttributes: attr,
};
let size = mem::size_of_val(&info);
try!(cvt(unsafe {
c::SetFileInformationByHandle(self.handle.raw(),
c::FileBasicInfo,
&mut info as *mut _ as *mut _,
size as c::DWORD)
}));
Ok(())
}

pub fn set_perm(&self, perm: FilePermissions) -> io::Result<()> {
let attr = try!(self.file_attr()).attributes;
if attr & c::FILE_ATTRIBUTE_DIRECTORY != 0 {
// this matches directories, dir symlinks, and junctions.
if perm.readonly {
Err(io::Error::new(io::ErrorKind::PermissionDenied,
"directories can not be read-only"))
} else {
Ok(()) // no reason to fail, as the result is what is expected.
// (the directory will not be read-only)
}
} else {
if perm.readonly == (attr & c::FILE_ATTRIBUTE_READONLY != 0) {
Ok(())
} else if perm.readonly {
self.set_attributes(attr | c::FILE_ATTRIBUTE_READONLY)
} else {
self.set_attributes(attr & !c::FILE_ATTRIBUTE_READONLY)
}
}
}

pub fn duplicate(&self) -> io::Result<File> {
Ok(File {
handle: try!(self.handle.duplicate(0, true, c::DUPLICATE_SAME_ACCESS)),
Expand Down Expand Up @@ -422,7 +507,12 @@ impl FileAttr {
}

pub fn perm(&self) -> FilePermissions {
FilePermissions { attrs: self.attributes }
FilePermissions {
// Only files can be read-only. If the flag is set on a directory this means the
// directory its view is customized by Windows (with a Desktop.ini file)
readonly: self.attributes & c::FILE_ATTRIBUTE_READONLY != 0 &&
self.attributes & c::FILE_ATTRIBUTE_DIRECTORY == 0
}
}

pub fn attrs(&self) -> u32 { self.attributes as u32 }
Expand Down Expand Up @@ -465,17 +555,9 @@ fn to_u64(ft: &c::FILETIME) -> u64 {
}

impl FilePermissions {
pub fn readonly(&self) -> bool {
self.attrs & c::FILE_ATTRIBUTE_READONLY != 0
}

pub fn set_readonly(&mut self, readonly: bool) {
if readonly {
self.attrs |= c::FILE_ATTRIBUTE_READONLY;
} else {
self.attrs &= !c::FILE_ATTRIBUTE_READONLY;
}
}
pub fn new() -> FilePermissions { Default::default() }
pub fn readonly(&self) -> bool { self.readonly }
pub fn set_readonly(&mut self, readonly: bool) { self.readonly = readonly }
}

impl FileType {
Expand All @@ -502,9 +584,6 @@ impl FileType {
*self == FileType::SymlinkDir ||
*self == FileType::MountPoint
}
pub fn is_symlink_dir(&self) -> bool {
*self == FileType::SymlinkDir || *self == FileType::MountPoint
}
}

impl DirBuilder {
Expand Down Expand Up @@ -567,23 +646,85 @@ pub fn remove_dir_all(path: &Path) -> io::Result<()> {
// rmdir only deletes dir symlinks and junctions, not file symlinks.
rmdir(path)
} else {
remove_dir_all_recursive(path)
// canonicalize to get a `//?/`-path, which can handle files like `CON`
// and `morse .. .`, and when a directory structure is so deep it needs
// long path names
let path = try!(path.canonicalize());
let base_dir = match path.parent() {
Some(dir) => dir,
None => return Err(io::Error::new(io::ErrorKind::PermissionDenied,
"can't delete root directory"))
};
try!(remove_dir_all_recursive(path.as_ref(), base_dir.as_ref(), 0));
Ok(())
}
}

fn remove_dir_all_recursive(path: &Path) -> io::Result<()> {
fn remove_dir_all_recursive(path: &Path, base_dir: &Path, mut counter: u64)
-> io::Result<u64> {
Copy link
Member

Choose a reason for hiding this comment

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

Stylistically we typically align the -> with the first character after the (

// On Windows it is not enough to just recursively remove the contents of a
// directory and then the directory itself. Deleting does not happen
// instantaneously, but is scheduled. So just before or after flagging a
// file for deletion, we move it to some `base_dir` to avoid races.
Copy link
Member

Choose a reason for hiding this comment

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

This implementation is getting pretty complicated, could you expand this comment to explain all the cases we're trying to catch here? It'll be nice to have a big intro to what's going on below.


fn move_item(file: &File, base_dir: &Path, mut counter: u64)
-> io::Result<u64> {
let mut tmpname = base_dir.join(format!{"rm-{}", counter});
Copy link
Member

Choose a reason for hiding this comment

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

Stylistically we typically use parens on format! macro invocations rather than braces

counter += 1;
// Try to rename the file. If it already exists, just retry with an other filename.
while let Err(err) = file.rename(tmpname.as_ref(), false) {
if err.kind() != io::ErrorKind::AlreadyExists { return Err(err) };
tmpname = base_dir.join(format!{"rm-{}", counter});
counter += 1;
}
Ok(counter)
}

fn remove_item(path: &Path,
base_dir: &Path,
mut counter: u64,
readonly: bool) -> io::Result<u64> {
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps this could take a FilePermissions instead of readonly to be a little easier to read below?

if !readonly {
let mut opts = OpenOptions::new();
opts.access_mode(c::DELETE);
opts.custom_flags(c::FILE_FLAG_BACKUP_SEMANTICS | // delete directory
c::FILE_FLAG_OPEN_REPARSE_POINT | // delete symlink
c::FILE_FLAG_DELETE_ON_CLOSE);
Copy link
Member

Choose a reason for hiding this comment

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

Just to confirm, the mode here is:

  • This will fail if there are any other option handles which do not specific FILE_SHARE_DELETE
  • All other future openings will fail unless they specify FILE_SHARE_DELETE if this succeeds.
  • Once all handles are closed, the file is deleted

So in other words, so long as nothing deadlocks, we're guaranteed to delete the file if the open succeeds, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, except all future openings will fail no matter what options or share mode they specify.

let file = try!(File::open(path, &opts));
counter = try!(move_item(&file, base_dir, counter));
} else {
let mut opts = OpenOptions::new();
opts.access_mode(c::DELETE | c::FILE_WRITE_ATTRIBUTES);
opts.custom_flags(c::FILE_FLAG_OPEN_REPARSE_POINT);
let file = try!(File::open(path, &opts));
// remove read-only permision
try!(file.set_perm(FilePermissions::new()));
counter = try!(move_item(&file, base_dir, counter));
try!(file.remove());
Copy link
Member

Choose a reason for hiding this comment

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

Could the file be removed before moving? I'm a little worried about moving a file and then the process crashes or something, so these rm-{tag} files get littered all over the place

Copy link
Contributor Author

Choose a reason for hiding this comment

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

According to the documentation there is just about nothing you can do with the handle after remove() except close it. I am a little surprised restoring the read-only flag works. But moving after flagging for deletion does not work.

Copy link
Member

Choose a reason for hiding this comment

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

In that case maybe it'd be better to open, set the bits, close, reopen with "close on delete", and then move?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't like to open the file twice, but you are right.

// restore read-only flag just in case there are other hard links
let mut perm = FilePermissions::new();
perm.set_readonly(true);
try!(file.set_perm(perm));
};
Ok(counter)
}
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 move these helper functions to the end of the function body? I tend to find it helpful to read the code and then go look at the implementation details.


for child in try!(readdir(path)) {
let child = try!(child);
let child_type = try!(child.file_type());
if child_type.is_dir() {
try!(remove_dir_all_recursive(&child.path()));
} else if child_type.is_symlink_dir() {
try!(rmdir(&child.path()));
counter = try!(remove_dir_all_recursive(&child.path(),
base_dir,
counter));
} else {
try!(unlink(&child.path()));
counter = try!(remove_item(&child.path().as_ref(),
base_dir,
counter,
try!(child.metadata()).perm().readonly()));
}
}
rmdir(path)
counter = try!(remove_item(path, base_dir, counter, false));
Copy link
Member

Choose a reason for hiding this comment

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

Managing this counter variable may be best done with some sort of Context structure or something like that so it doesn't have to constantly be passed in and reset.

Ok(counter)
}

pub fn readlink(path: &Path) -> io::Result<PathBuf> {
Expand Down Expand Up @@ -640,12 +781,12 @@ pub fn lstat(path: &Path) -> io::Result<FileAttr> {
file.file_attr()
}

pub fn set_perm(p: &Path, perm: FilePermissions) -> io::Result<()> {
let p = try!(to_u16s(p));
unsafe {
try!(cvt(c::SetFileAttributesW(p.as_ptr(), perm.attrs)));
Ok(())
}
pub fn set_perm(path: &Path, perm: FilePermissions) -> io::Result<()> {
let mut opts = OpenOptions::new();
opts.access_mode(c::FILE_READ_ATTRIBUTES | c::FILE_WRITE_ATTRIBUTES);
opts.custom_flags(c::FILE_FLAG_BACKUP_SEMANTICS);
let file = try!(File::open(path, &opts));
file.set_perm(perm)
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 add some tests for this as well? It's fine for some of them to be windows-specific (e.g. in this module), but specifically:

  • Directories can't be set to readonly
  • Other attributes are preserved

}

fn get_path(f: &File) -> io::Result<PathBuf> {
Expand Down