use anyhow::{Context, Result, bail};
use std::path::{Component, Path, PathBuf};
pub fn resolve_workspace_path(workspace_root: &Path, candidate: &Path) -> Result<PathBuf> {
let mut resolved = if candidate.is_absolute() {
PathBuf::new()
} else {
workspace_root.to_path_buf()
};
for component in candidate.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
resolved.pop();
}
Component::Normal(part) => resolved.push(part),
Component::RootDir => resolved.push(component.as_os_str()),
Component::Prefix(prefix) => resolved.push(prefix.as_os_str()),
}
}
if !resolved.starts_with(workspace_root) {
bail!("path {} escapes the workspace root", candidate.display());
}
Ok(resolved)
}
pub fn display_workspace_relative(workspace_root: &Path, path: &Path) -> String {
let relative = path.strip_prefix(workspace_root).unwrap_or(path);
if relative.as_os_str().is_empty() {
".".to_string()
} else {
relative.display().to_string()
}
}
pub(super) fn read_existing_text(path: &Path) -> Result<String> {
match std::fs::read_to_string(path) {
Ok(contents) => Ok(contents),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(String::new()),
Err(error) => Err(error).with_context(|| format!("failed to read {}", path.display())),
}
}
pub(super) fn truncate_in_place(value: &mut String, max_bytes: usize) {
if value.len() <= max_bytes {
return;
}
let mut end = max_bytes;
while end > 0 && !value.is_char_boundary(end) {
end -= 1;
}
value.truncate(end);
value.push_str("\n[truncated]");
}