-
Notifications
You must be signed in to change notification settings - Fork 13k
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
Fix fs::remove_dir_all
#31944
Changes from 3 commits
13bad94
3d65bb2
5f9ca88
9bb385e
89489ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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)] | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Nowhere do I see any mention of -1. What error do you get when you use TRUE instead of -1? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you move this comment to where it's called down in |
||
} | ||
|
||
#[repr(C)] | ||
pub struct FILE_END_OF_FILE_INFO { | ||
pub EndOfFile: LARGE_INTEGER, | ||
|
@@ -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; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
||
|
@@ -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; | ||
|
||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this be done with There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, perhaps this could be something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this an existing macro? I can't find it... There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: |
||
|
||
// FIXME: check for internal NULs in 'new' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have taken a look at There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)), | ||
|
@@ -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 } | ||
|
@@ -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 { | ||
|
@@ -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 { | ||
|
@@ -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> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stylistically we typically align 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stylistically we typically use parens on |
||
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> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps this could take a |
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to confirm, the mode here is:
So in other words, so long as nothing deadlocks, we're guaranteed to delete the file if the open succeeds, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Managing this |
||
Ok(counter) | ||
} | ||
|
||
pub fn readlink(path: &Path) -> io::Result<PathBuf> { | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
|
||
} | ||
|
||
fn get_path(f: &File) -> io::Result<PathBuf> { | ||
|
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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
?