use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
pub(crate) static NEXT_SUBAGENT_ID: AtomicU64 = AtomicU64::new(1);
static NEXT_TMP_SEQ: AtomicU64 = AtomicU64::new(0);
pub(crate) fn unique_tmp_path(path: &Path) -> PathBuf {
let seq = NEXT_TMP_SEQ.fetch_add(1, Ordering::Relaxed);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let mut name = path
.file_name()
.map(|n| n.to_os_string())
.unwrap_or_default();
name.push(format!(".{}.{}.{}.agent-tmp", std::process::id(), nanos, seq));
path.with_file_name(name)
}
pub(crate) fn strip_ansi(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\x1b' {
match chars.peek() {
Some('[') => {
chars.next();
while let Some(&c) = chars.peek() {
chars.next();
if c.is_ascii_alphabetic() || c == '~' || c == '@' {
break;
}
}
}
Some(']') => {
chars.next();
while let Some(&c) = chars.peek() {
chars.next();
if c == '\x07' {
break;
}
if c == '\x1b' {
if chars.peek() == Some(&'\\') {
chars.next();
}
break;
}
}
}
Some(_) => {
chars.next();
} None => {}
}
} else {
result.push(ch);
}
}
result
}
pub(crate) fn expand_path(path: &str) -> PathBuf {
if path.starts_with("~/") {
if let Ok(home) = std::env::var("HOME") {
return PathBuf::from(home).join(path.strip_prefix("~/").unwrap());
}
} else if path == "~" {
if let Ok(home) = std::env::var("HOME") {
return PathBuf::from(home);
}
}
PathBuf::from(path)
}
pub(crate) fn resolve_path_in(raw: &str, cwd: Option<&std::path::Path>) -> PathBuf {
let expanded = expand_path(raw);
match cwd {
Some(base) if expanded.is_relative() => base.join(expanded),
_ => expanded,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn resolve_path_in_anchors_relative_to_cwd() {
let base = std::path::Path::new("/tmp/synaps-cwd-test");
assert_eq!(
resolve_path_in("rel/file.txt", Some(base)),
base.join("rel/file.txt")
);
assert_eq!(
resolve_path_in("/abs/file.txt", Some(base)),
PathBuf::from("/abs/file.txt")
);
}
#[test]
fn resolve_path_in_none_is_byte_identical_to_expand_path() {
for raw in ["rel/file.txt", "./x", "/abs", "~/foo", "~"] {
assert_eq!(resolve_path_in(raw, None), expand_path(raw), "{raw}");
}
}
#[cfg(unix)]
#[test]
fn canonical_path_key_resolves_directory_symlink_alias_for_existing_leaf() {
use std::os::unix::fs::symlink;
let tmp = tempfile::TempDir::new().unwrap();
let real = tmp.path().join("real");
std::fs::create_dir(&real).unwrap();
std::fs::write(real.join("file.txt"), "x").unwrap();
let alias = tmp.path().join("alias");
symlink(&real, &alias).unwrap();
assert_eq!(
canonical_path_key(real.join("file.txt").to_str().unwrap()),
canonical_path_key(alias.join("file.txt").to_str().unwrap())
);
}
#[cfg(unix)]
#[test]
fn canonical_path_key_resolves_directory_symlink_alias_for_missing_leaf() {
use std::os::unix::fs::symlink;
let tmp = tempfile::TempDir::new().unwrap();
let real = tmp.path().join("real");
std::fs::create_dir(&real).unwrap();
let alias = tmp.path().join("alias");
symlink(&real, &alias).unwrap();
assert_eq!(
canonical_path_key(real.join("new/file.txt").to_str().unwrap()),
canonical_path_key(alias.join("new/file.txt").to_str().unwrap())
);
}
#[cfg(unix)]
#[test]
fn canonical_path_key_resolves_symlink_before_parent_traversal() {
use std::os::unix::fs::symlink;
let tmp = tempfile::TempDir::new().unwrap();
let a = tmp.path().join("a");
let b = tmp.path().join("b");
std::fs::create_dir(&a).unwrap();
std::fs::create_dir(&b).unwrap();
symlink(&b, a.join("link")).unwrap();
let through_link = canonical_path_key(a.join("link/../x.txt").to_str().unwrap());
assert_eq!(
through_link,
canonical_path_key(tmp.path().join("x.txt").to_str().unwrap()),
"same actual kernel target must share one serialization key"
);
assert_ne!(
through_link,
canonical_path_key(a.join("x.txt").to_str().unwrap()),
"distinct actual targets must not share a key via lexical .. collapse"
);
}
#[cfg(unix)]
#[test]
fn canonical_path_key_symlink_parent_traversal_lands_in_target_parent() {
use std::os::unix::fs::symlink;
let tmp = tempfile::TempDir::new().unwrap();
let nest = tmp.path().join("deep/nest");
std::fs::create_dir_all(&nest).unwrap();
let link = tmp.path().join("l");
symlink(&nest, &link).unwrap();
assert_eq!(
canonical_path_key(link.join("../f.txt").to_str().unwrap()),
canonical_path_key(tmp.path().join("deep/f.txt").to_str().unwrap()),
"l/../f must key to the symlink target's parent, deep/f"
);
}
#[cfg(unix)]
#[test]
fn canonical_path_key_parent_traversal_after_missing_components() {
use std::os::unix::fs::symlink;
let tmp = tempfile::TempDir::new().unwrap();
let real = tmp.path().join("real");
std::fs::create_dir(&real).unwrap();
assert_eq!(
canonical_path_key(real.join("m1/m2/../f.txt").to_str().unwrap()),
canonical_path_key(real.join("m1/f.txt").to_str().unwrap()),
".. inside the missing suffix pops the missing component"
);
let b_link = tmp.path().join("b_link");
symlink(&real, &b_link).unwrap();
assert_eq!(
canonical_path_key(b_link.join("miss/../x.txt").to_str().unwrap()),
canonical_path_key(real.join("x.txt").to_str().unwrap()),
);
}
#[cfg(unix)]
#[test]
fn canonical_path_key_dangling_symlink_falls_back_to_conservative_lane() {
use std::os::unix::fs::symlink;
let tmp = tempfile::TempDir::new().unwrap();
let dangling = tmp.path().join("dangling");
symlink(tmp.path().join("void"), &dangling).unwrap();
assert_eq!(
canonical_path_key(dangling.join("f.txt").to_str().unwrap()),
None,
"unresolvable symlink component must yield the conservative lane"
);
}
#[test]
fn canonical_path_key_root_parent_stays_at_root() {
let tmp = tempfile::TempDir::new().unwrap();
let under_root = format!("/..{}", tmp.path().join("r.txt").display());
assert_eq!(
canonical_path_key(&under_root),
canonical_path_key(tmp.path().join("r.txt").to_str().unwrap()),
);
}
#[test]
fn test_expand_path_home_prefix() {
let home = env::var("HOME").expect("HOME env var should be set");
let result = expand_path("~/foo");
assert_eq!(result, PathBuf::from(home).join("foo"));
}
#[test]
fn test_expand_path_tilde_alone() {
let home = env::var("HOME").expect("HOME env var should be set");
let result = expand_path("~");
assert_eq!(result, PathBuf::from(home));
}
#[test]
fn test_expand_path_absolute_unchanged() {
let result = expand_path("/absolute/path");
assert_eq!(result, PathBuf::from("/absolute/path"));
}
#[test]
fn test_expand_path_relative_unchanged() {
let result = expand_path("relative/path");
assert_eq!(result, PathBuf::from("relative/path"));
}
}
pub(crate) fn canonical_path_key(raw: &str) -> Option<String> {
if raw.trim().is_empty() {
return None;
}
let expanded = expand_path(raw);
let absolute = if expanded.is_absolute() {
expanded
} else {
std::env::current_dir().ok()?.join(expanded)
};
use std::path::Component;
let mut resolved = std::path::PathBuf::new();
let mut suffix: Vec<std::ffi::OsString> = Vec::new();
for component in absolute.components() {
match component {
Component::Prefix(prefix) => resolved.push(prefix.as_os_str()),
Component::RootDir => resolved.push(component.as_os_str()),
Component::CurDir => {}
Component::ParentDir => {
if suffix.pop().is_some() {
continue;
}
let is_root = resolved.parent().is_none();
if !is_root && !resolved.pop() {
return None;
}
}
Component::Normal(name) => {
if !suffix.is_empty() {
suffix.push(name.to_os_string());
continue;
}
let candidate = resolved.join(name);
match std::fs::symlink_metadata(&candidate) {
Ok(_) => {
resolved = std::fs::canonicalize(&candidate).ok()?;
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
suffix.push(name.to_os_string());
}
Err(_) => return None,
}
}
}
}
for component in suffix {
resolved.push(component);
}
Some(resolved.to_string_lossy().to_string())
}