use std::path::{Component, Path, PathBuf};
pub fn resolve_in(workspace: &Path, input: &str, must_exist: bool) -> Result<PathBuf, String> {
if input.is_empty() {
return Err("path must not be empty".into());
}
let workspace_canon = workspace
.canonicalize()
.map_err(|e| format!("workspace not accessible: {e}"))?;
let raw = PathBuf::from(input);
for c in raw.components() {
if matches!(c, Component::ParentDir) {
return Err("path traversal (..) is not allowed".into());
}
}
let candidate = if raw.is_absolute() {
raw
} else {
workspace_canon.join(&raw)
};
let resolved = if candidate.exists() {
candidate
.canonicalize()
.map_err(|e| format!("cannot resolve path {}: {e}", candidate.display()))?
} else if must_exist {
return Err(format!("file not found: {}", candidate.display()));
} else {
let parent = candidate
.parent()
.ok_or_else(|| "path has no parent".to_string())?;
if !parent.exists() {
return Err(format!(
"parent directory does not exist: {}",
parent.display()
));
}
let parent_canon = parent
.canonicalize()
.map_err(|e| format!("cannot resolve parent {}: {e}", parent.display()))?;
let file_name = candidate
.file_name()
.ok_or_else(|| "path has no file name".to_string())?;
parent_canon.join(file_name)
};
if !resolved.starts_with(&workspace_canon) {
return Err(format!(
"path {} escapes workspace {}",
resolved.display(),
workspace_canon.display()
));
}
Ok(resolved)
}
pub fn resolve_subfolder(workspace: &Path, input: &str) -> Result<PathBuf, String> {
let trimmed = input.trim_matches('/').trim();
if trimmed.is_empty() || trimmed == "." {
return workspace
.canonicalize()
.map_err(|e| format!("workspace not accessible: {e}"));
}
resolve_in(workspace, trimmed, true)
}