use crate::support::path::lexical_normalize;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy)]
pub(in crate::tools) struct ExistingPathPolicy {
allow_absolute_paths: bool,
setting_name: &'static str,
}
impl ExistingPathPolicy {
pub(in crate::tools) fn read(allow_absolute_paths: bool) -> Self {
Self {
allow_absolute_paths,
setting_name: "tools.read.absolute_paths",
}
}
pub(in crate::tools) fn view_image(allow_absolute_paths: bool) -> Self {
Self {
allow_absolute_paths,
setting_name: "tools.view_image.absolute_paths",
}
}
pub(in crate::tools) fn hash_edit(allow_absolute_paths: bool) -> Self {
Self {
allow_absolute_paths,
setting_name: "tools.hash_edit.absolute_paths",
}
}
pub(in crate::tools) fn grep(allow_absolute_paths: bool) -> Self {
Self {
allow_absolute_paths,
setting_name: "tools.grep.absolute_paths",
}
}
pub(in crate::tools) fn find(allow_absolute_paths: bool) -> Self {
Self {
allow_absolute_paths,
setting_name: "tools.find.absolute_paths",
}
}
pub(in crate::tools) fn list_files(allow_absolute_paths: bool) -> Self {
Self {
allow_absolute_paths,
setting_name: "tools.list_files.absolute_paths",
}
}
pub(in crate::tools) fn ast_grep(allow_absolute_paths: bool) -> Self {
Self {
allow_absolute_paths,
setting_name: "tools.ast_grep.absolute_paths",
}
}
}
pub(in crate::tools) fn resolve_existing_path_with_policy(
cwd: &Path,
cwd_canonical: &Path,
path: &str,
policy: ExistingPathPolicy,
) -> anyhow::Result<PathBuf> {
let user_path = PathBuf::from(path);
let is_absolute = user_path.is_absolute();
let candidate = if is_absolute {
user_path
} else {
cwd.join(user_path)
};
let canonical = candidate.canonicalize().map_err(|error| {
super::path_suggestions::with_suggestions(
error,
&candidate,
cwd_canonical,
is_absolute && policy.allow_absolute_paths,
)
})?;
if (!is_absolute || !policy.allow_absolute_paths) && !canonical.starts_with(cwd_canonical) {
anyhow::bail!(
"path '{}' escapes cwd '{}' ({} does not allow this path)",
canonical.display(),
cwd_canonical.display(),
policy.setting_name
);
}
Ok(canonical)
}
#[derive(Clone, Copy)]
pub(in crate::tools) struct PathContext<'a> {
pub(in crate::tools) cwd: &'a PathBuf,
pub(in crate::tools) cwd_canonical: &'a PathBuf,
}
impl PathContext<'_> {
pub(in crate::tools) fn resolve_existing_path(
&self,
path: &str,
policy: ExistingPathPolicy,
) -> anyhow::Result<PathBuf> {
resolve_existing_path_with_policy(self.cwd, self.cwd_canonical, path, policy)
}
pub(in crate::tools) fn ensure_inside_with_setting(
&self,
canonical: &Path,
setting_name: &str,
) -> anyhow::Result<()> {
if !canonical.starts_with(self.cwd_canonical) {
anyhow::bail!(
"path '{}' escapes cwd '{}' ({} does not allow this path)",
canonical.display(),
self.cwd_canonical.display(),
setting_name
);
}
Ok(())
}
}
impl super::WriteContext<'_> {
pub(in crate::tools) fn resolve_for_write(&self, path: &str) -> anyhow::Result<PathBuf> {
let user_path = PathBuf::from(path);
let is_absolute = user_path.is_absolute();
let allow_absolute = self.settings.absolute_paths && is_absolute;
let candidate = if is_absolute {
user_path
} else {
self.paths.cwd.join(user_path)
};
if let Ok(metadata) = fs::symlink_metadata(&candidate) {
if metadata.file_type().is_symlink() {
anyhow::bail!(
"write target '{}' must not be a symlink",
candidate.display()
);
}
let canonical = candidate.canonicalize()?;
if !allow_absolute {
self.paths
.ensure_inside_with_setting(&canonical, "tools.write.absolute_paths")?;
}
return Ok(canonical);
}
let normalized = lexical_normalize(&candidate);
if !allow_absolute && !normalized.starts_with(self.paths.cwd_canonical) {
anyhow::bail!(
"path '{}' escapes cwd '{}' (set tools.write.absolute_paths=true to allow absolute paths)",
normalized.display(),
self.paths.cwd_canonical.display()
);
}
let parent = normalized
.parent()
.ok_or_else(|| anyhow::anyhow!("path has no parent"))?;
let existing_parent = nearest_existing_ancestor(parent)?;
let parent_canonical = existing_parent.canonicalize()?;
if !allow_absolute {
self.paths
.ensure_inside_with_setting(&parent_canonical, "tools.write.absolute_paths")?;
}
Ok(normalized)
}
pub(super) fn ensure_parent_inside_after_creation(
&self,
parent: &Path,
allow_absolute_paths: bool,
) -> anyhow::Result<()> {
let canonical = parent.canonicalize()?;
if allow_absolute_paths {
return Ok(());
}
self.paths
.ensure_inside_with_setting(&canonical, "tools.write.absolute_paths")
}
}
fn nearest_existing_ancestor(path: &Path) -> anyhow::Result<PathBuf> {
let mut cursor = path;
loop {
if cursor.exists() {
return Ok(cursor.to_path_buf());
}
cursor = cursor
.parent()
.ok_or_else(|| anyhow::anyhow!("no existing ancestor for '{}'", path.display()))?;
}
}