#![forbid(unsafe_code)]
use crate::{
error::{self, Error, ErrorExt},
resolvers::Resolver,
syscalls,
utils::RawFdExt,
Handle,
};
use std::{
fs::{File, Permissions},
os::unix::{ffi::OsStrExt, fs::PermissionsExt, io::AsRawFd},
path::Path,
};
use libc::{c_int, dev_t};
use snafu::{OptionExt, ResultExt};
#[derive(Copy, Clone, Debug)]
pub enum InodeType<'a> {
File(&'a Permissions),
Directory(&'a Permissions),
Symlink(&'a Path),
Hardlink(&'a Path),
Fifo(&'a Permissions),
CharacterDevice(&'a Permissions, dev_t),
BlockDevice(&'a Permissions, dev_t),
}
fn path_split(path: &'_ Path) -> Result<(&'_ Path, &'_ Path), Error> {
let parent = path.parent().unwrap_or_else(|| "/".as_ref());
let name = path.file_name().context(error::InvalidArgument {
name: "path",
description: "no trailing component",
})?;
ensure!(
!name.as_bytes().contains(&b'/'),
error::SafetyViolation {
description: "trailing component of split pathname contains '/'",
}
);
Ok((parent, name.as_ref()))
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct RenameFlags(pub c_int);
impl RenameFlags {
pub fn supported(self) -> bool {
self.0 == 0 || *syscalls::RENAME_FLAGS_SUPPORTED
}
}
#[derive(Debug)]
pub struct Root {
pub(crate) inner: File,
pub resolver: Resolver,
}
impl Root {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let file = syscalls::openat(libc::AT_FDCWD, path, libc::O_PATH | libc::O_DIRECTORY, 0)
.context(error::RawOsError {
operation: "open root handle",
})?;
Ok(Root::from_file_unchecked(file))
}
pub fn try_clone(&self) -> Result<Self, Error> {
Ok(Self {
inner: self.inner.try_clone_hotfix()?,
resolver: self.resolver,
})
}
pub fn into_file(self) -> File {
self.inner
}
pub fn from_file_unchecked(inner: File) -> Self {
Self {
inner,
resolver: Default::default(),
}
}
#[inline]
pub fn resolve<P: AsRef<Path>>(&self, path: P) -> Result<Handle, Error> {
self.resolver.resolve(&self.inner, path)
}
pub fn create<P: AsRef<Path>>(&self, path: P, inode_type: &InodeType) -> Result<(), Error> {
if let InodeType::File(perm) = inode_type {
return self.create_file(path, perm).map(|_| ());
}
let (parent, name) =
path_split(path.as_ref()).wrap("split target path into (parent, name)")?;
let dir = self
.resolve(parent)
.wrap("resolve target parent directory for inode creation")?
.inner;
let dirfd = dir.as_raw_fd();
match inode_type {
InodeType::File(_) => unreachable!(),
InodeType::Directory(perm) => {
let mode = perm.mode() & !libc::S_IFMT;
syscalls::mkdirat(dirfd, name, mode)
}
InodeType::Symlink(target) => {
syscalls::symlinkat(target, dirfd, &name)
}
InodeType::Hardlink(target) => {
let (oldparent, oldname) =
path_split(target).wrap("split hardlink source path into (parent, name)")?;
let olddir = self
.resolve(oldparent)
.wrap("resolve hardlink source parent for hardlink")?
.inner;
let olddirfd = olddir.as_raw_fd();
syscalls::linkat(olddirfd, oldname, dirfd, name, 0)
}
InodeType::Fifo(perm) => {
let mode = perm.mode() & !libc::S_IFMT;
syscalls::mknodat(dirfd, name, libc::S_IFIFO | mode, 0)
}
InodeType::CharacterDevice(perm, dev) => {
let mode = perm.mode() & !libc::S_IFMT;
syscalls::mknodat(dirfd, name, libc::S_IFCHR | mode, *dev)
}
InodeType::BlockDevice(perm, dev) => {
let mode = perm.mode() & !libc::S_IFMT;
syscalls::mknodat(dirfd, name, libc::S_IFBLK | mode, *dev)
}
}
.context(error::RawOsError {
operation: "pathrs create",
})
}
pub fn create_file<P: AsRef<Path>>(
&self,
path: P,
perm: &Permissions,
) -> Result<Handle, Error> {
let (parent, name) =
path_split(path.as_ref()).wrap("split target path into (parent, name)")?;
let dir = self
.resolve(parent)
.wrap("resolve target parent directory for inode creation")?
.inner;
let dirfd = dir.as_raw_fd();
let file = syscalls::openat(dirfd, name, libc::O_CREAT | libc::O_EXCL, perm.mode())
.context(error::RawOsError {
operation: "pathrs create_file",
})?;
Ok(Handle::from_file_unchecked(file))
}
pub fn remove<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
let (parent, name) =
path_split(path.as_ref()).wrap("split target path into (parent, name)")?;
let dir = self
.resolve(parent)
.wrap("resolve target parent directory for inode creation")?
.inner;
let dirfd = dir.as_raw_fd();
let mut last_error: Option<syscalls::Error> = None;
for _ in 0..16 {
let stat = match syscalls::fstatat(dirfd, name) {
Ok(stat) => stat,
Err(err) => {
last_error = Some(err);
continue;
}
};
let mut flags = 0;
if stat.st_mode & libc::S_IFMT == libc::S_IFDIR {
flags |= libc::AT_REMOVEDIR;
}
match syscalls::unlinkat(dirfd, name, flags) {
Ok(_) => return Ok(()),
Err(err) => {
last_error = Some(err);
continue;
}
}
}
Err(last_error.expect("unlinkat loop failed so last_error must exist")).context(
error::RawOsError {
operation: "pathrs remove",
},
)
}
pub fn rename<P: AsRef<Path>>(
&self,
source: P,
destination: P,
flags: RenameFlags,
) -> Result<(), Error> {
let (src_parent, src_name) =
path_split(source.as_ref()).wrap("split source path into (parent, name)")?;
let (dst_parent, dst_name) =
path_split(destination.as_ref()).wrap("split target path into (parent, name)")?;
let src_dir = self
.resolve(src_parent)
.wrap("resolve source path for rename")?
.inner;
let src_dirfd = src_dir.as_raw_fd();
let dst_dir = self
.resolve(dst_parent)
.wrap("resolve target path for rename")?
.inner;
let dst_dirfd = dst_dir.as_raw_fd();
syscalls::renameat2(src_dirfd, src_name, dst_dirfd, dst_name, flags.0).context(
error::RawOsError {
operation: "pathrs rename",
},
)
}
}