use std::ffi::{CString, OsString};
use std::fs::{File, OpenOptions};
use std::io;
use std::os::fd::{AsRawFd, FromRawFd};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Component, Path, PathBuf};
use anyhow::{Context, Result, bail};
#[cfg(target_os = "linux")]
const DIRECTORY_ACCESS: libc::c_int = libc::O_PATH;
#[cfg(not(target_os = "linux"))]
const DIRECTORY_ACCESS: libc::c_int = libc::O_SEARCH;
pub(super) struct CheckedDirectory {
handle: File,
path: PathBuf,
missing: Vec<OsString>,
}
impl CheckedDirectory {
pub(super) fn check(worktree: &Path, relative: &Path) -> Result<Self> {
let mut names = Vec::new();
for component in relative.components() {
let Component::Normal(name) = component else {
bail!(
"project skills directory {} is not worktree-relative",
relative.display()
);
};
names.push(name.to_owned());
}
let handle = OpenOptions::new()
.read(true)
.custom_flags(DIRECTORY_ACCESS | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC)
.open(worktree)
.with_context(|| format!("refusing project skills directory {}", worktree.display()))?;
let mut checked = Self {
handle,
path: worktree.to_owned(),
missing: Vec::new(),
};
for (at, name) in names.iter().enumerate() {
let path = checked.path.join(name);
match open_child(&checked.handle, Path::new(name)) {
Ok(handle) => {
checked.handle = handle;
checked.path = path;
}
Err(error) if error.kind() == io::ErrorKind::NotFound => {
checked.missing = names[at..].to_vec();
break;
}
Err(error) => {
return Err(error).with_context(|| {
format!("refusing project skills directory {}", path.display())
});
}
}
}
Ok(checked)
}
pub(super) fn create_missing(mut self) -> Result<Self> {
for name in std::mem::take(&mut self.missing) {
let path = self.path.join(&name);
let name = CString::new(name.as_bytes())?;
if unsafe { libc::mkdirat(self.handle.as_raw_fd(), name.as_ptr(), 0o777) } != 0 {
let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::AlreadyExists {
return Err(error)
.with_context(|| format!("could not create {}", path.display()));
}
}
self.handle = open_child(
&self.handle,
Path::new(std::ffi::OsStr::from_bytes(name.as_bytes())),
)
.with_context(|| format!("refusing project skills directory {}", path.display()))?;
self.path = path;
}
Ok(self)
}
pub(super) fn link(&self, target: &Path, name: &Path) -> Result<()> {
let path = self.path.join(name);
let name = CString::new(name.as_os_str().as_bytes())?;
let target = CString::new(target.as_os_str().as_bytes())?;
if unsafe { libc::unlinkat(self.handle.as_raw_fd(), name.as_ptr(), 0) } != 0 {
let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::NotFound {
return Err(error).with_context(|| format!("could not replace {}", path.display()));
}
}
if unsafe { libc::symlinkat(target.as_ptr(), self.handle.as_raw_fd(), name.as_ptr()) } != 0
{
return Err(io::Error::last_os_error())
.with_context(|| format!("could not link {}", path.display()));
}
Ok(())
}
}
fn open_child(parent: &File, name: &Path) -> io::Result<File> {
let name = CString::new(name.as_os_str().as_bytes())?;
let fd = unsafe {
libc::openat(
parent.as_raw_fd(),
name.as_ptr(),
DIRECTORY_ACCESS | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
)
};
if fd < 0 {
return Err(io::Error::last_os_error());
}
Ok(unsafe { File::from_raw_fd(fd) })
}