use std::collections::VecDeque;
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::io::{Read, Write};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
use std::path::{Component, Path, PathBuf};
use agent_client_protocol::Error as AcpError;
use rustix::fs::{AtFlags, FileType, Mode, OFlags, fstat, mkdirat, openat, readlinkat, statat};
use rustix::io::Errno;
use super::acp::{acp_error, outside_workspace_error, slice_lines};
const MAX_SYMLINK_HOPS: usize = 40;
const ACQUIRE_ATTEMPTS: usize = 8;
pub(super) struct PinnedRoot {
raw: PathBuf,
canonical: PathBuf,
fd: File,
}
impl PinnedRoot {
pub(super) fn acquire(root: &Path) -> Result<Self, AcpError> {
let mut last: Option<AcpError> = None;
for _ in 0..ACQUIRE_ATTEMPTS {
match Self::try_acquire(root) {
Ok(pinned) => return Ok(pinned),
Err(e) => last = Some(e),
}
}
Err(last.unwrap_or_else(|| acp_error("failed to pin the workspace root")))
}
fn try_acquire(root: &Path) -> Result<Self, AcpError> {
let inaccessible = |what: String, e: std::io::Error| {
acp_error(format!("session workspace '{what}' is not accessible: {e}"))
};
let fd = File::open(root).map_err(|e| inaccessible(root.display().to_string(), e))?;
let st_fd = fstat(&fd).map_err(|e| inaccessible(root.display().to_string(), e.into()))?;
let canonical = root
.canonicalize()
.map_err(|e| inaccessible(root.display().to_string(), e))?;
let st_path = std::fs::metadata(&canonical)
.map_err(|e| inaccessible(canonical.display().to_string(), e))?;
#[allow(clippy::cast_sign_loss, clippy::unnecessary_cast)]
if st_fd.st_dev as u64 != st_path.dev() || st_fd.st_ino != st_path.ino() {
return Err(acp_error(format!(
"session workspace '{}' changed while being pinned; retrying",
root.display()
)));
}
Ok(Self {
raw: root.to_path_buf(),
canonical,
fd,
})
}
fn relative_components(&self, requested: &Path) -> Result<VecDeque<OsString>, AcpError> {
let relative = requested
.strip_prefix(&self.canonical)
.or_else(|_| requested.strip_prefix(&self.raw))
.map_err(|_| outside_workspace_error(requested))?;
let mut queue = VecDeque::new();
for component in relative.components() {
match component {
Component::Normal(name) => queue.push_back(name.to_os_string()),
Component::CurDir => {}
_ => return Err(outside_workspace_error(requested)),
}
}
Ok(queue)
}
}
pub(super) fn read(
root: &PinnedRoot,
requested: &Path,
line: Option<u32>,
limit: Option<u32>,
) -> Result<String, AcpError> {
let mut file = open_pinned(root, requested, false)?;
let mut content = String::new();
file.read_to_string(&mut content)
.map_err(|e| acp_error(format!("failed to read {}: {e}", requested.display())))?;
Ok(slice_lines(&content, line, limit))
}
pub(super) fn write(root: &PinnedRoot, requested: &Path, content: &str) -> Result<(), AcpError> {
let mut file = open_pinned(root, requested, true)?;
file.write_all(content.as_bytes())
.map_err(|e| acp_error(format!("failed to write {}: {e}", requested.display())))?;
Ok(())
}
fn dir_flags() -> OFlags {
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC
}
fn open_pinned(root: &PinnedRoot, requested: &Path, create: bool) -> Result<File, AcpError> {
let mut queue = root.relative_components(requested)?;
let mut dir = root
.fd
.try_clone()
.map_err(|e| acp_error(format!("failed to pin the workspace root: {e}")))?;
let mut hops = 0usize;
loop {
let Some(name) = queue.pop_front() else {
return Err(acp_error(format!(
"path '{}' does not name a file inside the workspace",
requested.display()
)));
};
let last = queue.is_empty();
if last {
let flags = if create {
OFlags::WRONLY | OFlags::CREATE | OFlags::TRUNC | OFlags::NOFOLLOW | OFlags::CLOEXEC
} else {
OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC
};
return match openat(&dir, &name, flags, Mode::from_raw_mode(0o644)) {
Ok(fd) => Ok(File::from(fd)),
Err(e) if e == Errno::NOENT && !create => Err(acp_error(format!(
"failed to read {}: file does not exist",
requested.display()
))),
Err(e) if e == Errno::LOOP && create => Err(outside_workspace_error(requested)),
Err(e) if e == Errno::LOOP => {
hops += 1;
if hops > MAX_SYMLINK_HOPS {
return Err(outside_workspace_error(requested));
}
follow_symlink(&mut dir, root, &name, requested, &mut queue)?;
continue;
}
Err(e) => Err(acp_error(format!(
"failed to open {}: {e}",
requested.display()
))),
};
}
if statat(&dir, &name, AtFlags::SYMLINK_NOFOLLOW)
.is_ok_and(|st| FileType::from_raw_mode(st.st_mode) == FileType::Symlink)
{
hops += 1;
if hops > MAX_SYMLINK_HOPS {
return Err(outside_workspace_error(requested));
}
follow_symlink(&mut dir, root, &name, requested, &mut queue)?;
continue;
}
match openat(&dir, &name, dir_flags(), Mode::empty()) {
Ok(fd) => dir = File::from(fd),
Err(e) if e == Errno::NOENT && create => {
if let Err(e) = mkdirat(&dir, &name, Mode::from_raw_mode(0o755))
&& e != Errno::EXIST
{
return Err(acp_error(format!(
"failed to create directory '{}': {e}",
name.to_string_lossy()
)));
}
dir = File::from(
openat(&dir, &name, dir_flags(), Mode::empty()).map_err(|e| {
acp_error(format!(
"failed to open directory '{}': {e}",
name.to_string_lossy()
))
})?,
);
}
Err(e) if e == Errno::NOENT => {
return Err(acp_error(format!(
"failed to read {}: file does not exist",
requested.display()
)));
}
Err(e) if e == Errno::LOOP => {
hops += 1;
if hops > MAX_SYMLINK_HOPS {
return Err(outside_workspace_error(requested));
}
follow_symlink(&mut dir, root, &name, requested, &mut queue)?;
}
Err(e) => {
return Err(acp_error(format!(
"failed to traverse '{}': {e}",
name.to_string_lossy()
)));
}
}
}
}
fn follow_symlink(
dir: &mut File,
root: &PinnedRoot,
name: &OsStr,
requested: &Path,
queue: &mut VecDeque<OsString>,
) -> Result<(), AcpError> {
let target =
readlinkat(&*dir, name, Vec::new()).map_err(|_| outside_workspace_error(requested))?;
let target_path = PathBuf::from(OsStr::from_bytes(target.to_bytes()));
if target_path.is_absolute() {
let relative = target_path
.strip_prefix(&root.canonical)
.map_err(|_| outside_workspace_error(requested))?;
if relative
.components()
.any(|c| matches!(c, Component::ParentDir))
{
return Err(outside_workspace_error(requested));
}
for component in relative.components().rev() {
queue.push_front(component.as_os_str().to_os_string());
}
*dir = root
.fd
.try_clone()
.map_err(|e| acp_error(format!("failed to reset the walk base: {e}")))?;
} else {
if target_path
.components()
.any(|c| matches!(c, Component::ParentDir))
{
return Err(outside_workspace_error(requested));
}
for component in target_path.components().rev() {
queue.push_front(component.as_os_str().to_os_string());
}
}
Ok(())
}