use std::path::{Component, Path, PathBuf};
pub fn expand_home(path: &str) -> String {
if path == "~" {
return dirs::home_dir()
.map(|h| h.to_string_lossy().to_string())
.unwrap_or_else(|| path.to_string());
}
if let Some(rest) = path.strip_prefix("~/")
&& let Some(home) = dirs::home_dir()
{
return format!("{}/{}", home.display(), rest);
}
if let Some(rest) = path.strip_prefix("$HOME/")
&& let Some(home) = dirs::home_dir()
{
return format!("{}/{}", home.display(), rest);
}
if path == "$HOME" {
return dirs::home_dir()
.map(|h| h.to_string_lossy().to_string())
.unwrap_or_else(|| path.to_string());
}
path.to_string()
}
pub fn strip_curdir(path: &Path) -> PathBuf {
path.components()
.filter(|c| !matches!(c, Component::CurDir))
.collect()
}
pub fn normalize_path(path: &Path) -> PathBuf {
let mut components = Vec::new();
for component in path.components() {
match component {
Component::CurDir => {} Component::ParentDir => {
if let Some(last) = components.last()
&& !matches!(last, Component::RootDir | Component::Prefix(_))
{
components.pop();
continue;
}
components.push(component);
}
_ => components.push(component),
}
}
components.iter().collect()
}
fn strip_hallucinated_prefix(path_str: &str, working_dir: &Path) -> String {
for prefix in HALLUCINATED_PREFIXES {
if let Some(rest) = path_str.strip_prefix(prefix) {
let original = Path::new(path_str);
if !original.exists() {
let candidate = working_dir.join(rest);
if candidate.exists() || candidate.parent().map(|p| p.is_dir()).unwrap_or(false) {
return candidate.to_string_lossy().to_string();
}
return candidate.to_string_lossy().to_string();
}
}
}
let bare_prefixes = ["/workspace", "/testbed"];
for prefix in &bare_prefixes {
if path_str == *prefix && !Path::new(prefix).exists() {
return working_dir.to_string_lossy().to_string();
}
}
path_str.to_string()
}
const HALLUCINATED_PREFIXES: &[&str] = &["/workspace/", "/testbed/"];
pub fn resolve_file_path(user_path: &str, working_dir: &Path) -> PathBuf {
let expanded = expand_home(user_path);
let expanded = strip_hallucinated_prefix(&expanded, working_dir);
let path = strip_curdir(Path::new(&expanded));
let path = normalize_path(&path);
let path = path.as_path();
if path.is_absolute() {
if path.exists() {
return path.to_path_buf();
}
if let Ok(rel) = path.strip_prefix(working_dir)
&& let Some(first) = rel.components().next()
{
let first_name = first.as_os_str();
if working_dir
.file_name()
.map(|n| n == first_name)
.unwrap_or(false)
{
let fixed = working_dir.join(rel.strip_prefix(first_name).unwrap_or(rel));
if fixed.exists() || fixed.parent().map(|p| p.is_dir()).unwrap_or(false) {
return fixed;
}
}
}
path.to_path_buf()
} else {
let joined = normalize_path(&working_dir.join(path));
if joined.exists() {
return joined;
}
let mut components = path.components();
if let Some(first) = components.next() {
let first_name = first.as_os_str();
if working_dir
.file_name()
.map(|n| n == first_name)
.unwrap_or(false)
{
let rest: PathBuf = components.collect();
if !rest.as_os_str().is_empty() {
let fixed = normalize_path(&working_dir.join(&rest));
if fixed.exists() || fixed.parent().map(|p| p.is_dir()).unwrap_or(false) {
return fixed;
}
}
}
}
joined
}
}
pub fn resolve_dir_path(user_path: &str, working_dir: &Path) -> PathBuf {
let expanded = expand_home(user_path);
let expanded = strip_hallucinated_prefix(&expanded, working_dir);
let path = strip_curdir(Path::new(&expanded));
let path = normalize_path(&path);
let path = path.as_path();
if path.is_absolute() {
if path.is_dir() {
return path.to_path_buf();
}
if let Ok(rel) = path.strip_prefix(working_dir)
&& let Some(first) = rel.components().next()
{
let first_name = first.as_os_str();
if working_dir
.file_name()
.map(|n| n == first_name)
.unwrap_or(false)
{
let fixed = working_dir.join(rel.strip_prefix(first_name).unwrap_or(rel));
if fixed.is_dir() || fixed.parent().map(|p| p.is_dir()).unwrap_or(false) {
return fixed;
}
}
}
if working_dir.starts_with(path) || working_dir == path {
working_dir.to_path_buf()
} else {
path.to_path_buf()
}
} else {
let joined = normalize_path(&working_dir.join(path));
if joined.is_dir() {
return joined;
}
let mut components = path.components();
if let Some(first) = components.next() {
let first_name = first.as_os_str();
if working_dir
.file_name()
.map(|n| n == first_name)
.unwrap_or(false)
{
let rest: PathBuf = components.collect();
if rest.as_os_str().is_empty() {
return working_dir.to_path_buf();
}
let fixed = working_dir.join(&rest);
if fixed.is_dir() || fixed.parent().map(|p| p.is_dir()).unwrap_or(false) {
return fixed;
}
}
}
joined
}
}
#[cfg(test)]
#[path = "path_tests.rs"]
mod tests;