use std::ffi::CString;
use std::fs::File;
use std::io;
use std::os::fd::{AsRawFd, FromRawFd};
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
pub(super) fn open(root: &Path, relative: &Path) -> io::Result<File> {
let directory = open_directory(root)?;
let mut directory = directory;
let mut components = relative.components().peekable();
let file = loop {
let Some(component) = components.next() else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"relative path has no final component",
));
};
let component_name = component.as_os_str();
let name = CString::new(component_name.as_bytes()).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"path component {} contains NUL",
Path::new(component_name).display()
),
)
})?;
let flags =
libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_NONBLOCK | libc::O_NOCTTY;
let fd = unsafe { libc::openat(directory.as_raw_fd(), name.as_ptr(), flags) };
if fd < 0 {
let error = io::Error::last_os_error().with_context(|| {
format!(
"failed to open path component {} below {}",
Path::new(component_name).display(),
root.display()
)
});
return Err(error);
}
let opened = unsafe { File::from_raw_fd(fd) };
if components.peek().is_some() {
if !opened.metadata()?.is_dir() {
return Err(io::Error::new(
io::ErrorKind::NotADirectory,
format!(
"path component {} is not a directory",
Path::new(component_name).display()
),
));
}
directory = opened;
} else {
break opened;
}
};
if !file.metadata()?.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("path {} is not a regular file", relative.display()),
));
}
Ok(file)
}
fn open_directory(root: &Path) -> io::Result<File> {
let root = CString::new(root.as_os_str().as_bytes()).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("root path {} contains NUL", root.display()),
)
})?;
let flags = libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW;
let fd = unsafe { libc::open(root.as_ptr(), flags) };
if fd < 0 {
return Err(io::Error::last_os_error());
}
let directory = unsafe { File::from_raw_fd(fd) };
if !directory.metadata()?.is_dir() {
return Err(io::Error::new(
io::ErrorKind::NotADirectory,
"root path is not a directory",
));
}
Ok(directory)
}
trait IoContext {
fn with_context<F>(self, context: F) -> Self
where
F: FnOnce() -> String;
}
impl IoContext for io::Error {
fn with_context<F>(self, context: F) -> Self
where
F: FnOnce() -> String,
{
let message = format!("{}: {}", context(), self);
if self.raw_os_error().is_some() {
self
} else {
io::Error::new(self.kind(), message)
}
}
}