278 lines
9.6 KiB
Rust
278 lines
9.6 KiB
Rust
use std::ffi::OsString;
|
|
use std::fs::{self, File, OpenOptions};
|
|
use std::os::windows::prelude::*;
|
|
use std::path::{Path, PathBuf};
|
|
use std::{io, ptr};
|
|
|
|
use winapi::shared::minwindef::*;
|
|
use winapi::shared::winerror::*;
|
|
use winapi::um::errhandlingapi::*;
|
|
use winapi::um::fileapi::*;
|
|
use winapi::um::minwinbase::*;
|
|
use winapi::um::winbase::*;
|
|
use winapi::um::winnt::*;
|
|
|
|
pub const VOLUME_NAME_DOS: DWORD = 0x0;
|
|
|
|
struct RmdirContext<'a> {
|
|
base_dir: &'a Path,
|
|
readonly: bool,
|
|
counter: u64,
|
|
}
|
|
|
|
/// Reliably removes a directory and all of its children.
|
|
///
|
|
/// ```rust
|
|
/// extern crate remove_dir_all;
|
|
///
|
|
/// use std::fs;
|
|
/// use remove_dir_all::*;
|
|
///
|
|
/// fn main() {
|
|
/// fs::create_dir("./temp/").unwrap();
|
|
/// remove_dir_all("./temp/").unwrap();
|
|
/// }
|
|
/// ```
|
|
pub fn remove_dir_all<P: AsRef<Path>>(path: P) -> io::Result<()> {
|
|
// 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.
|
|
// To work around this, we move the file or directory to some `base_dir`
|
|
// right before deletion to avoid races.
|
|
//
|
|
// As `base_dir` we choose the parent dir of the directory we want to
|
|
// remove. We very probably have permission to create files here, as we
|
|
// already need write permission in this dir to delete the directory. And it
|
|
// should be on the same volume.
|
|
//
|
|
// To handle files with names like `CON` and `morse .. .`, and when a
|
|
// directory structure is so deep it needs long path names the path is first
|
|
// converted to a `//?/`-path with `get_path()`.
|
|
//
|
|
// To make sure we don't leave a moved file laying around if the process
|
|
// crashes before we can delete the file, we do all operations on an file
|
|
// handle. By opening a file with `FILE_FLAG_DELETE_ON_CLOSE` Windows will
|
|
// always delete the file when the handle closes.
|
|
//
|
|
// All files are renamed to be in the `base_dir`, and have their name
|
|
// changed to "rm-<counter>". After every rename the counter is increased.
|
|
// Rename should not overwrite possibly existing files in the base dir. So
|
|
// if it fails with `AlreadyExists`, we just increase the counter and try
|
|
// again.
|
|
//
|
|
// For read-only files and directories we first have to remove the read-only
|
|
// attribute before we can move or delete them. This also removes the
|
|
// attribute from possible hardlinks to the file, so just before closing we
|
|
// restore the read-only attribute.
|
|
//
|
|
// If 'path' points to a directory symlink or junction we should not
|
|
// recursively remove the target of the link, but only the link itself.
|
|
//
|
|
// Moving and deleting is guaranteed to succeed if we are able to open the
|
|
// file with `DELETE` permission. If others have the file open we only have
|
|
// `DELETE` permission if they have specified `FILE_SHARE_DELETE`. We can
|
|
// also delete the file now, but it will not disappear until all others have
|
|
// closed the file. But no-one can open the file after we have flagged it
|
|
// for deletion.
|
|
|
|
// Open the path once to get the canonical path, file type and attributes.
|
|
let (path, metadata) = {
|
|
let path = path.as_ref();
|
|
let mut opts = OpenOptions::new();
|
|
opts.access_mode(FILE_READ_ATTRIBUTES);
|
|
opts.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT);
|
|
let file = opts.open(path)?;
|
|
(get_path(&file)?, path.metadata()?)
|
|
};
|
|
|
|
let mut ctx = RmdirContext {
|
|
base_dir: match path.parent() {
|
|
Some(dir) => dir,
|
|
None => {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::PermissionDenied,
|
|
"Can't delete root directory",
|
|
))
|
|
}
|
|
},
|
|
readonly: metadata.permissions().readonly(),
|
|
counter: 0,
|
|
};
|
|
|
|
let filetype = metadata.file_type();
|
|
if filetype.is_dir() {
|
|
if !filetype.is_symlink() {
|
|
remove_dir_all_recursive(path.as_ref(), &mut ctx)
|
|
} else {
|
|
remove_item(path.as_ref(), &mut ctx)
|
|
}
|
|
} else {
|
|
Err(io::Error::new(
|
|
io::ErrorKind::PermissionDenied,
|
|
"Not a directory",
|
|
))
|
|
}
|
|
}
|
|
|
|
fn remove_item(path: &Path, ctx: &mut RmdirContext) -> io::Result<()> {
|
|
if ctx.readonly {
|
|
// remove read-only permision
|
|
let mut permissions = path.metadata()?.permissions();
|
|
permissions.set_readonly(false);
|
|
|
|
fs::set_permissions(path, permissions)?;
|
|
}
|
|
|
|
let mut opts = OpenOptions::new();
|
|
opts.access_mode(DELETE);
|
|
opts.custom_flags(
|
|
FILE_FLAG_BACKUP_SEMANTICS | // delete directory
|
|
FILE_FLAG_OPEN_REPARSE_POINT | // delete symlink
|
|
FILE_FLAG_DELETE_ON_CLOSE,
|
|
);
|
|
let file = opts.open(path)?;
|
|
move_item(&file, ctx)?;
|
|
|
|
if ctx.readonly {
|
|
// restore read-only flag just in case there are other hard links
|
|
match fs::metadata(&path) {
|
|
Ok(metadata) => {
|
|
let mut perm = metadata.permissions();
|
|
perm.set_readonly(true);
|
|
fs::set_permissions(&path, perm)?;
|
|
}
|
|
Err(ref err) if err.kind() == io::ErrorKind::NotFound => {}
|
|
err => return err.map(|_| ()),
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn move_item(file: &File, ctx: &mut RmdirContext) -> io::Result<()> {
|
|
let mut tmpname = ctx.base_dir.join(format! {"rm-{}", ctx.counter});
|
|
ctx.counter += 1;
|
|
|
|
// Try to rename the file. If it already exists, just retry with an other
|
|
// filename.
|
|
while let Err(err) = rename(file, &tmpname, false) {
|
|
if err.kind() != io::ErrorKind::AlreadyExists {
|
|
return Err(err);
|
|
};
|
|
tmpname = ctx.base_dir.join(format!("rm-{}", ctx.counter));
|
|
ctx.counter += 1;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn rename(file: &File, new: &Path, replace: bool) -> io::Result<()> {
|
|
// &self must be opened with DELETE permission
|
|
use std::iter;
|
|
#[cfg(target_pointer_width = "32")]
|
|
const STRUCT_SIZE: usize = 12;
|
|
#[cfg(target_pointer_width = "64")]
|
|
const STRUCT_SIZE: usize = 20;
|
|
|
|
// FIXME: check for internal NULs in 'new'
|
|
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 info = data.as_mut_ptr() as *mut FILE_RENAME_INFO;
|
|
// The type of ReplaceIfExists is BOOL, but it actually expects a
|
|
// BOOLEAN. This means true is -1, not c::TRUE.
|
|
(*info).ReplaceIfExists = if replace { -1 } else { FALSE };
|
|
(*info).RootDirectory = ptr::null_mut();
|
|
(*info).FileNameLength = (size - STRUCT_SIZE) as DWORD;
|
|
let result = SetFileInformationByHandle(
|
|
file.as_raw_handle(),
|
|
FileRenameInfo,
|
|
data.as_mut_ptr() as *mut _ as *mut _,
|
|
size as DWORD,
|
|
);
|
|
|
|
if result == 0 {
|
|
Err(io::Error::last_os_error())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_path(f: &File) -> io::Result<PathBuf> {
|
|
fill_utf16_buf(
|
|
|buf, sz| unsafe { GetFinalPathNameByHandleW(f.as_raw_handle(), buf, sz, VOLUME_NAME_DOS) },
|
|
|buf| PathBuf::from(OsString::from_wide(buf)),
|
|
)
|
|
}
|
|
|
|
fn remove_dir_all_recursive(path: &Path, ctx: &mut RmdirContext) -> io::Result<()> {
|
|
let dir_readonly = ctx.readonly;
|
|
for child in fs::read_dir(path)? {
|
|
let child = child?;
|
|
let child_type = child.file_type()?;
|
|
ctx.readonly = child.metadata()?.permissions().readonly();
|
|
if child_type.is_dir() {
|
|
remove_dir_all_recursive(&child.path(), ctx)?;
|
|
} else {
|
|
remove_item(&child.path().as_ref(), ctx)?;
|
|
}
|
|
}
|
|
ctx.readonly = dir_readonly;
|
|
remove_item(path, ctx)
|
|
}
|
|
|
|
fn fill_utf16_buf<F1, F2, T>(mut f1: F1, f2: F2) -> io::Result<T>
|
|
where
|
|
F1: FnMut(*mut u16, DWORD) -> DWORD,
|
|
F2: FnOnce(&[u16]) -> T,
|
|
{
|
|
// Start off with a stack buf but then spill over to the heap if we end up
|
|
// needing more space.
|
|
let mut stack_buf = [0u16; 512];
|
|
let mut heap_buf = Vec::new();
|
|
unsafe {
|
|
let mut n = stack_buf.len();
|
|
|
|
loop {
|
|
let buf = if n <= stack_buf.len() {
|
|
&mut stack_buf[..]
|
|
} else {
|
|
let extra = n - heap_buf.len();
|
|
heap_buf.reserve(extra);
|
|
heap_buf.set_len(n);
|
|
&mut heap_buf[..]
|
|
};
|
|
|
|
// This function is typically called on windows API functions which
|
|
// will return the correct length of the string, but these functions
|
|
// also return the `0` on error. In some cases, however, the
|
|
// returned "correct length" may actually be 0!
|
|
//
|
|
// To handle this case we call `SetLastError` to reset it to 0 and
|
|
// then check it again if we get the "0 error value". If the "last
|
|
// error" is still 0 then we interpret it as a 0 length buffer and
|
|
// not an actual error.
|
|
SetLastError(0);
|
|
let k = match f1(buf.as_mut_ptr(), n as DWORD) {
|
|
0 if GetLastError() == 0 => 0,
|
|
0 => return Err(io::Error::last_os_error()),
|
|
n => n,
|
|
} as usize;
|
|
if k == n && GetLastError() == ERROR_INSUFFICIENT_BUFFER {
|
|
n *= 2;
|
|
} else if k >= n {
|
|
n = k;
|
|
} else {
|
|
return Ok(f2(&buf[..k]));
|
|
}
|
|
}
|
|
}
|
|
}
|