use std::{
ffi::OsStr,
io,
num::NonZeroUsize,
os::unix::ffi::OsStrExt,
path::{Component, Path, PathBuf},
};
use rustix::{
fd::{AsFd, BorrowedFd, OwnedFd},
fs::{
chmodat, chownat, mkdirat, openat, openat2, unlinkat, AtFlags, Gid, Mode, OFlags,
ResolveFlags, Uid,
},
io::Errno,
path::Arg,
};
pub(crate) struct Directory {
fd: OwnedFd,
}
impl From<OwnedFd> for Directory {
fn from(fd: OwnedFd) -> Self {
Directory { fd }
}
}
impl AsFd for Directory {
fn as_fd(&self) -> BorrowedFd<'_> {
self.fd.as_fd()
}
}
impl Directory {
pub fn new<P: Arg>(target: P) -> Result<Self, Errno> {
let fd = openat(
rustix::fs::CWD,
target,
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
Mode::empty(),
)?;
Ok(Directory { fd })
}
pub fn create<P: Arg>(&self, path: P, mode: Mode) -> Result<OwnedFd, Errno> {
openat2(
self,
path,
OFlags::CREATE | OFlags::EXCL | OFlags::WRONLY,
mode,
ResolveFlags::BENEATH,
)
}
pub fn tmpfile(&self) -> Result<OwnedFd, Errno> {
openat(
self,
c".",
OFlags::TMPFILE | OFlags::RDWR | OFlags::EXCL,
Mode::RUSR | Mode::WUSR,
)
}
pub fn open_directory<P>(&self, path: P, create: bool) -> Result<OwnedFd, Errno>
where
P: AsRef<Path>,
{
let path = path.as_ref();
loop {
let result = openat2(
self,
path,
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
Mode::empty(),
ResolveFlags::IN_ROOT | ResolveFlags::NO_MAGICLINKS,
);
match result {
Err(e) if create && e.kind() == io::ErrorKind::NotFound => (),
r => return r,
}
let file_name = match path.file_name() {
Some(f) => f,
None => return Err(Errno::NOENT),
};
let owned_slot;
let parent = match path.parent() {
Some(p) if p == Path::new("") => &self.fd,
None => &self.fd,
Some(p) => {
owned_slot = self.open_directory(p, create)?;
&owned_slot
}
};
mkdirat(parent.as_fd(), file_name, Mode::from_raw_mode(0o755))?;
}
}
}
pub(crate) struct DirFdCache<'a> {
directory: &'a Directory,
cache: lru::LruCache<PathBuf, OwnedFd>,
}
const FDS_CACHE: usize = 16;
impl<'a> DirFdCache<'a> {
pub fn new(directory: &'a Directory) -> Self {
let cache = lru::LruCache::new(NonZeroUsize::new(FDS_CACHE).unwrap());
DirFdCache { directory, cache }
}
pub fn get<P>(&mut self, path: P, create: bool) -> Result<BorrowedFd, Errno>
where
P: AsRef<Path>,
{
let path = path.as_ref();
self.cache
.try_get_or_insert_ref(path, || self.directory.open_directory(path, create))
.map(|fd| fd.as_fd())
}
pub fn clear(&mut self) {
self.cache.clear();
}
}
pub fn normalize_path<T: AsRef<Path>>(path: T) -> io::Result<(PathBuf, PathBuf)> {
let mut parent_path = PathBuf::from("/");
let mut file_name = None;
for component in path.as_ref().components() {
match component {
Component::Prefix(..) | Component::RootDir | Component::CurDir => continue,
Component::ParentDir => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Found '..' in the path.",
));
}
Component::Normal(part) => {
if let Some(previous) = file_name.take() {
parent_path.push(previous);
}
file_name = Some(part)
}
}
}
let file_name = match file_name {
Some(file_name) => PathBuf::from(file_name),
None => PathBuf::from("."),
};
Ok((parent_path, file_name))
}
pub fn change_owner(
parent_fd: BorrowedFd,
file_name: &Path,
uid: Option<u32>,
gid: Option<u32>,
preserve_mode: bool,
) -> io::Result<()> {
if uid.is_none() && gid.is_none() {
return Ok(());
}
let orig_mode = if preserve_mode {
Some(rustix::fs::statat(parent_fd, file_name, AtFlags::SYMLINK_NOFOLLOW)?.st_mode)
} else {
None
};
let result = chownat(
parent_fd,
file_name,
uid.map(|id| unsafe { Uid::from_raw(id) }),
gid.map(|id| unsafe { Gid::from_raw(id) }),
AtFlags::SYMLINK_NOFOLLOW,
);
if let (Ok(_), Some(mode)) = (result, orig_mode) {
if mode & 0o7000 != 0 {
chmodat(
parent_fd,
file_name,
Mode::from_bits_retain(mode),
AtFlags::empty(),
)?;
}
}
Ok(())
}
#[derive(Copy, Clone, PartialEq)]
pub enum RemovedEntry {
Directory,
Nothing,
Other,
}
pub fn remove_entry(parent_fd: BorrowedFd, path: &Path) -> io::Result<RemovedEntry> {
match unlinkat(parent_fd, path, AtFlags::empty()) {
Ok(()) => Ok(RemovedEntry::Other),
Err(Errno::ISDIR) => {
remove_subtree(parent_fd, path)?;
match unlinkat(parent_fd, path, AtFlags::REMOVEDIR) {
Err(Errno::NOENT) => Ok(RemovedEntry::Nothing),
Err(e) => Err(e.into()),
_ => Ok(RemovedEntry::Directory),
}
}
Err(Errno::NOENT) => Ok(RemovedEntry::Nothing),
Err(e) => Err(e.into()),
}
}
pub fn remove_subtree(parent_fd: BorrowedFd, path: &Path) -> io::Result<()> {
let subdir = openat2(
parent_fd,
path,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW,
Mode::empty(),
ResolveFlags::BENEATH,
)?;
let mut entries = rustix::fs::Dir::read_from(&subdir)?;
while let Some(Ok(entry)) = entries.read() {
let name = entry.file_name().to_bytes();
if name != b"." && name != b".." {
remove_entry(subdir.as_fd(), Path::new(OsStr::from_bytes(name)))?;
}
}
Ok(())
}