use crate::{
error::{Error, ErrorExt, ErrorImpl},
flags::OpenFlags,
syscalls,
};
use std::{
ffi::OsStr,
os::unix::{ffi::OsStrExt, io::AsFd},
path::Path,
};
use rustix::fs::{AtFlags, Dir};
trait RmdirResultExt {
fn ignore_enoent(self) -> Self;
}
impl RmdirResultExt for Result<(), Error> {
fn ignore_enoent(self) -> Self {
match self.map_err(|err| (err.kind().errno(), err)) {
Ok(()) | Err((Some(libc::ENOENT), _)) => Ok(()),
Err((_, err)) => Err(err),
}
}
}
fn remove_inode(dirfd: impl AsFd, name: impl AsRef<Path>) -> Result<(), Error> {
let dirfd = dirfd.as_fd();
let name = name.as_ref();
syscalls::unlinkat(dirfd, name, AtFlags::empty())
.or_else(|unlink_err| {
syscalls::unlinkat(dirfd, name, AtFlags::REMOVEDIR).map_err(|rmdir_err| {
if rmdir_err.root_cause().raw_os_error() == Some(libc::ENOTDIR) {
unlink_err
} else {
rmdir_err
}
})
})
.map_err(|err| {
ErrorImpl::RawOsError {
operation: "remove inode".into(),
source: err,
}
.into()
})
}
pub(crate) fn remove_all(dirfd: impl AsFd, name: impl AsRef<Path>) -> Result<(), Error> {
let dirfd = dirfd.as_fd();
let name = name.as_ref();
if name.as_os_str().as_bytes().contains(&b'/') {
Err(ErrorImpl::SafetyViolation {
description: "remove_all reached a component containing '/'".into(),
})?;
}
if remove_inode(dirfd, name).ignore_enoent().is_ok() {
return Ok(());
}
let subdir = match syscalls::openat(dirfd, name, OpenFlags::O_DIRECTORY, 0).map_err(|err| {
ErrorImpl::RawOsError {
operation: "open directory to scan entries".into(),
source: err,
}
}) {
Ok(fd) => fd,
Err(err) => match err.kind().errno() {
Some(libc::ENOENT) => return Ok(()),
_ => Err(err)?,
},
};
loop {
let mut iter = match Dir::read_from(&subdir)
.map_err(|err| ErrorImpl::OsError {
operation: "create directory iterator".into(),
source: err.into(),
})
.with_wrap(|| format!("scan directory {name:?} for deletion"))
{
Ok(iter) => iter,
Err(err) => match err.kind().errno() {
Some(libc::ENOENT) => break,
_ => Err(err)?,
},
}
.filter(|res| {
!matches!(
res.as_ref().map(|dentry| dentry.file_name().to_bytes()),
Ok(b".") | Ok(b"..")
)
})
.peekable();
if iter.peek().is_none() {
break;
}
for child in iter {
let child = child.map_err(|err| ErrorImpl::OsError {
operation: format!("scan directory {name:?}").into(),
source: err.into(),
})?;
let name: &Path = OsStr::from_bytes(child.file_name().to_bytes()).as_ref();
remove_all(&subdir, name).ignore_enoent()?
}
}
remove_inode(dirfd, name)
.ignore_enoent()
.with_wrap(|| format!("deleting emptied directory {name:?}"))
}
#[cfg(test)]
mod tests {
use super::remove_all;
use crate::{error::ErrorKind, tests::common as tests_common, Root};
use std::{os::unix::io::OwnedFd, path::Path};
use anyhow::Error;
use pretty_assertions::assert_eq;
#[test]
fn remove_all_basic() -> Result<(), Error> {
let dir = tests_common::create_basic_tree()?;
let dirfd: OwnedFd = Root::open(&dir)?.into();
assert_eq!(
remove_all(&dirfd, Path::new("a")).map_err(|err| err.kind()),
Ok(()),
"removeall(root, 'a') should work",
);
assert_eq!(
remove_all(&dirfd, Path::new("b")).map_err(|err| err.kind()),
Ok(()),
"removeall(root, 'b') should work",
);
assert_eq!(
remove_all(&dirfd, Path::new("c")).map_err(|err| err.kind()),
Ok(()),
"removeall(root, 'c') should work",
);
let _dir = dir; Ok(())
}
#[test]
fn remove_all_slash_path() -> Result<(), Error> {
let dir = tests_common::create_basic_tree()?;
let dirfd: OwnedFd = Root::open(&dir)?.into();
assert_eq!(
remove_all(&dirfd, Path::new("/")).map_err(|err| err.kind()),
Err(ErrorKind::SafetyViolation),
"removeall(root, '/') should fail",
);
assert_eq!(
remove_all(&dirfd, Path::new("./a")).map_err(|err| err.kind()),
Err(ErrorKind::SafetyViolation),
"removeall(root, './a') should fail",
);
assert_eq!(
remove_all(&dirfd, Path::new("a/")).map_err(|err| err.kind()),
Err(ErrorKind::SafetyViolation),
"removeall(root, 'a/') should fail",
);
let _dir = dir; Ok(())
}
}