mod bash;
mod edit;
mod glob;
mod grep;
mod read;
mod write;
pub use bash::BashTool;
pub use edit::EditTool;
pub use glob::GlobTool;
pub use grep::GrepTool;
pub use read::ReadTool;
pub use write::WriteTool;
use std::path::{Path, PathBuf};
pub fn validate_workspace_path(workspace_root: &Path, user_path: &str) -> Result<PathBuf, String> {
let resolved = workspace_root.join(user_path);
let canonical_root = std::fs::canonicalize(workspace_root)
.map_err(|e| format!("cannot canonicalize workspace root: {e}"))?;
if let Ok(canonical) = std::fs::canonicalize(&resolved) {
return if canonical.starts_with(&canonical_root) {
Ok(canonical)
} else {
Err(format!(
"path '{}' resolves outside the workspace",
user_path
))
};
}
let mut ancestor = resolved.as_path();
let mut suffix_components: Vec<std::ffi::OsString> = Vec::new();
while let Some(parent) = ancestor.parent() {
if let Some(name) = ancestor.file_name() {
suffix_components.push(name.to_os_string());
}
if let Ok(canonical_ancestor) = std::fs::canonicalize(parent) {
suffix_components.reverse();
let suffix: PathBuf = suffix_components.iter().collect();
let canonical = canonical_ancestor.join(suffix);
return if canonical.starts_with(&canonical_root) {
Ok(canonical)
} else {
Err(format!(
"path '{}' resolves outside the workspace",
user_path
))
};
}
ancestor = parent;
}
let normalized = normalize_path_components(&resolved);
if normalized.starts_with(&canonical_root) {
Ok(normalized)
} else {
Err(format!(
"path '{}' resolves outside the workspace",
user_path
))
}
}
fn normalize_path_components(path: &Path) -> PathBuf {
let mut stack = Vec::new();
for component in path.components() {
match component {
std::path::Component::ParentDir => {
stack.pop();
}
std::path::Component::CurDir => {}
c => stack.push(c.as_os_str()),
}
}
stack.iter().collect()
}