use crate::util::{other_io_error, ErrorContext, ResultErrorContext};
use std::ffi::{CStr, CString};
use std::fs::File;
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::{fmt, io};
pub fn openat(dir_fd: &impl AsRawFd, path: &str, flags: libc::c_int) -> io::Result<File> {
let path_cstr =
CString::new(path).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let fd = unsafe { libc::openat(dir_fd.as_raw_fd(), path_cstr.as_ptr(), flags) };
if fd >= 0 {
Ok(unsafe { File::from_raw_fd(fd) })
} else {
Err(io::Error::last_os_error())
}
}
pub fn openat_verbose(dir_fd: &impl AsRawFd, path: &str, flags: libc::c_int) -> io::Result<File> {
openat(dir_fd, path, flags).err_context(|| path)
}
pub fn reopen_fd_through_proc(
fd: &impl AsRawFd,
flags: libc::c_int,
proc_self_fd: &File,
) -> io::Result<File> {
openat(
proc_self_fd,
format!("{}", fd.as_raw_fd()).as_str(),
flags & !libc::O_NOFOLLOW,
)
}
pub fn is_safe_inode(mode: u32) -> bool {
matches!(mode & libc::S_IFMT, libc::S_IFREG | libc::S_IFDIR)
}
pub fn ebadf() -> io::Error {
io::Error::from_raw_os_error(libc::EBADF)
}
pub fn einval() -> io::Error {
io::Error::from_raw_os_error(libc::EINVAL)
}
pub fn erofs() -> io::Error {
io::Error::from_raw_os_error(libc::EROFS)
}
#[derive(Debug)]
pub(crate) enum FdPathError {
ReadLink(io::Error),
TooLong,
InvalidCString(io::Error),
NotAFile(String),
Deleted(String),
}
pub(crate) fn get_path_by_fd(
fd: &impl AsRawFd,
proc_self_fd: &impl AsRawFd,
) -> Result<CString, FdPathError> {
let fname = format!("{}\0", fd.as_raw_fd());
let fname_cstr = CStr::from_bytes_with_nul(fname.as_bytes()).unwrap();
let max_len = libc::PATH_MAX as usize; let mut link_target = vec![0u8; max_len + 1];
let ret = unsafe {
libc::readlinkat(
proc_self_fd.as_raw_fd(),
fname_cstr.as_ptr(),
link_target.as_mut_ptr().cast::<libc::c_char>(),
max_len,
)
};
if ret < 0 {
return Err(FdPathError::ReadLink(io::Error::last_os_error()));
} else if ret as usize == max_len {
return Err(FdPathError::TooLong);
}
link_target.truncate(ret as usize + 1);
let link_target_cstring = CString::from_vec_with_nul(link_target)
.map_err(|err| FdPathError::InvalidCString(other_io_error(err)))?;
let link_target_str = link_target_cstring.to_string_lossy();
let pre_slash = link_target_str.split('/').next().unwrap();
if pre_slash.contains(':') {
return Err(FdPathError::NotAFile(link_target_str.into_owned()));
}
if let Some(path) = link_target_str.strip_suffix(" (deleted)") {
return Err(FdPathError::Deleted(path.to_owned()));
}
Ok(link_target_cstring)
}
impl From<FdPathError> for io::Error {
fn from(err: FdPathError) -> Self {
match err {
FdPathError::ReadLink(err) => err.context("readlink"),
FdPathError::TooLong => other_io_error("Path returned from readlink is too long"),
FdPathError::InvalidCString(err) => err.context("readlink returned invalid path"),
FdPathError::NotAFile(path) => other_io_error(format!("Not a file ({path})")),
FdPathError::Deleted(path) => other_io_error(format!("Inode deleted ({path})")),
}
}
}
impl fmt::Display for FdPathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FdPathError::ReadLink(err) => write!(f, "readlink: {err}"),
FdPathError::TooLong => write!(f, "Path returned from readlink is too long"),
FdPathError::InvalidCString(err) => write!(f, "readlink returned invalid path: {err}"),
FdPathError::NotAFile(path) => write!(f, "Not a file ({path})"),
FdPathError::Deleted(path) => write!(f, "Inode deleted ({path})"),
}
}
}
impl std::error::Error for FdPathError {}
pub fn printable_fd(fd: &impl AsRawFd, proc_self_fd: Option<&impl AsRawFd>) -> String {
if let Some(Ok(path)) = proc_self_fd.map(|psf| get_path_by_fd(fd, psf)) {
match path.into_string() {
Ok(s) => s,
Err(err) => err.into_cstring().to_string_lossy().into_owned(),
}
} else {
format!("{{fd:{}}}", fd.as_raw_fd())
}
}
pub fn relative_path<'a>(path: &'a CStr, prefix: &CStr) -> io::Result<&'a CStr> {
let mut relative_path = path
.to_bytes_with_nul()
.strip_prefix(prefix.to_bytes())
.ok_or_else(|| {
other_io_error(format!(
"Path {path:?} is outside the directory ({prefix:?})"
))
})?;
while let Some(prefixless) = relative_path.strip_prefix(b"/") {
relative_path = prefixless;
}
Ok(CStr::from_bytes_with_nul(relative_path).unwrap())
}