#![allow(clippy::manual_string_new)]
use std::path::{Path, PathBuf};
use crate::cm_tools::workspace::fs::OpenedWorkspaceFile;
use crate::cm_tools::workspace::path::{
WorkspacePathError, absolutize_relative_under_root, ensure_canonical_within_root,
ensure_existing_ancestor_within_root,
};
pub use crate::cm_tools::workspace::path::canonical_workspace_root;
#[must_use]
pub fn tool_user_error_from_workspace_path(e: WorkspacePathError) -> String {
format!("错误:{}", e.user_message())
}
pub fn normalize_subpath_for_workspace(
working_dir: &Path,
sub: &str,
) -> Result<String, WorkspacePathError> {
let sub = sub.trim();
if sub.is_empty() {
return Err(WorkspacePathError::EmptyPath);
}
if !Path::new(sub).is_absolute() {
return Ok(sub.to_string());
}
let base = canonical_workspace_root(working_dir)?;
let p = Path::new(sub);
let mut try_path: &Path = p;
let resolved: std::path::PathBuf = loop {
match try_path.canonicalize() {
Ok(anchor) => {
let rel_suffix = p.strip_prefix(try_path).map_err(|_| {
WorkspacePathError::PathResolveFailed(std::io::Error::other(
"absolute path prefix mismatch during workspace normalization",
))
})?;
break anchor.join(rel_suffix);
}
Err(e) => {
try_path = try_path
.parent()
.ok_or(WorkspacePathError::PathResolveFailed(e))?;
}
}
};
ensure_canonical_within_root(&resolved, &base)?;
let rel = resolved
.strip_prefix(&base)
.map_err(|_| WorkspacePathError::OutsideWorkspaceRoot)?;
let s = rel.to_string_lossy().replace('\\', "/");
Ok(if s.is_empty() { ".".to_string() } else { s })
}
pub fn resolve_for_read(base: &Path, sub: &str) -> Result<PathBuf, WorkspacePathError> {
Ok(resolve_for_read_open(base, sub)?.resolved_path)
}
pub fn resolve_for_read_open(
base: &Path,
sub: &str,
) -> Result<OpenedWorkspaceFile, WorkspacePathError> {
let sub = normalize_subpath_for_workspace(base, sub)?;
let sub = sub.trim();
if sub.is_empty() {
return Err(WorkspacePathError::EmptyPath);
}
if Path::new(sub).is_absolute() {
return Err(WorkspacePathError::AbsolutePathNotAllowed);
}
let base_canonical = canonical_workspace_root(base)?;
let joined = base_canonical.join(sub);
let canonical = joined
.canonicalize()
.map_err(WorkspacePathError::PathResolveFailed)?;
ensure_canonical_within_root(&canonical, &base_canonical)?;
crate::cm_tools::workspace::fs::open_existing_file_under_root(&base_canonical, &canonical).map_err(|e| {
WorkspacePathError::PathResolveFailed(std::io::Error::new(
e.kind(),
format!("open under workspace root: {e}"),
))
})
}
pub(super) fn resolve_for_write(base: &Path, sub: &str) -> Result<PathBuf, WorkspacePathError> {
let sub = normalize_subpath_for_workspace(base, sub)?;
let sub = sub.trim();
if sub.is_empty() {
return Err(WorkspacePathError::EmptyPath);
}
if Path::new(sub).is_absolute() {
return Err(WorkspacePathError::AbsolutePathNotAllowed);
}
let base_canonical = canonical_workspace_root(base)?;
let normalized = absolutize_relative_under_root(&base_canonical, sub)?;
ensure_existing_ancestor_within_root(&base_canonical, &normalized)?;
Ok(normalized)
}
fn path_relative_to_workspace(working_dir: &Path, absolute: &Path) -> String {
let Ok(base) = canonical_workspace_root(working_dir) else {
return absolute.display().to_string();
};
match absolute.strip_prefix(&base) {
Ok(rel) => {
let s = rel.to_string_lossy().replace('\\', "/");
if s.is_empty() { ".".to_string() } else { s }
}
Err(_) => absolute.display().to_string(),
}
}
pub(super) fn path_for_tool_display(
working_dir: &Path,
absolute: &Path,
user_rel: Option<&str>,
) -> String {
if let Some(s) = user_rel {
let t = s.trim();
if !t.is_empty() {
return t.replace('\\', "/");
}
}
path_relative_to_workspace(working_dir, absolute)
}
pub(super) fn parse_path_content(args_json: &str) -> Result<(String, String), String> {
let v: serde_json::Value = crate::cm_tools::tools::parse_args_json(args_json)?;
let path = v
.get("path")
.and_then(|p| p.as_str())
.map(String::from)
.ok_or_else(|| "缺少 path 参数".to_string())?;
let content = v
.get("content")
.and_then(|c| c.as_str())
.map(String::from)
.unwrap_or_default();
Ok((path, content))
}