use dashmap::DashMap;
use std::path::{Component, Path, PathBuf};
use std::sync::OnceLock;
use std::time::Instant;
const MAX_TRACKED_WORKSPACES: usize = 2_000;
#[derive(Clone, Debug)]
struct WorkspaceEntry {
workspace: PathBuf,
last_touched: Instant,
}
fn workspaces() -> &'static DashMap<String, WorkspaceEntry> {
static WORKSPACES: OnceLock<DashMap<String, WorkspaceEntry>> = OnceLock::new();
WORKSPACES.get_or_init(DashMap::new)
}
pub fn set_workspace(session_id: &str, workspace: PathBuf) -> PathBuf {
let workspace = pin_via_provider(workspace);
let store = workspaces();
store.insert(
session_id.to_string(),
WorkspaceEntry {
workspace: workspace.clone(),
last_touched: Instant::now(),
},
);
evict_oldest_if_needed(store, MAX_TRACKED_WORKSPACES);
workspace
}
pub fn get_workspace(session_id: &str) -> Option<PathBuf> {
let mut entry = workspaces().get_mut(session_id)?;
entry.last_touched = Instant::now();
Some(entry.workspace.clone())
}
type DefaultWorkspaceProvider = Box<dyn Fn() -> Option<PathBuf> + Send + Sync>;
static DEFAULT_WORKSPACE_PROVIDER: OnceLock<DefaultWorkspaceProvider> = OnceLock::new();
pub fn set_default_workspace_provider(provider: DefaultWorkspaceProvider) {
let _ = DEFAULT_WORKSPACE_PROVIDER.set(provider);
}
pub fn get_configured_default_workspace() -> Option<PathBuf> {
DEFAULT_WORKSPACE_PROVIDER
.get()
.and_then(|provider| provider())
}
pub fn has_default_workspace_provider() -> bool {
DEFAULT_WORKSPACE_PROVIDER.get().is_some()
}
pub fn ensure_session_workspace(session_id: &str, preferred: Option<PathBuf>) -> Option<PathBuf> {
if let Some(workspace) = preferred {
return Some(set_workspace(session_id, workspace));
}
if let Some(existing) = get_workspace(session_id) {
return Some(existing);
}
if let Some(configured) = get_configured_default_workspace() {
return Some(set_workspace(session_id, configured));
}
None
}
pub fn workspace_or_process_cwd(session_id: Option<&str>) -> PathBuf {
if let Some(session_id) = session_id {
if let Some(workspace) = ensure_session_workspace(session_id, None) {
return workspace;
}
if let Some(provider) = WORKSPACE_ROOT_PROVIDER.get() {
let cfg = provider();
let dir = default_session_workspace_dir(&cfg.root, session_id);
return set_workspace(session_id, dir);
}
}
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}
#[derive(Clone, Debug)]
pub struct WorkspaceRootConfig {
pub root: PathBuf,
pub confine: bool,
}
type WorkspaceRootProvider = Box<dyn Fn() -> WorkspaceRootConfig + Send + Sync>;
static WORKSPACE_ROOT_PROVIDER: OnceLock<WorkspaceRootProvider> = OnceLock::new();
pub fn set_workspace_root_provider(provider: WorkspaceRootProvider) {
let _ = WORKSPACE_ROOT_PROVIDER.set(provider);
}
pub fn has_workspace_root_provider() -> bool {
WORKSPACE_ROOT_PROVIDER.get().is_some()
}
fn pin_via_provider(path: PathBuf) -> PathBuf {
match WORKSPACE_ROOT_PROVIDER.get() {
Some(provider) => {
let cfg = provider();
pin_workspace_path(&path, &cfg.root, cfg.confine)
}
None => path,
}
}
pub fn sanitize_path_component(raw: &str) -> String {
let cleaned: String = raw
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' || c == '.' {
c
} else {
'_'
}
})
.collect();
let trimmed = cleaned.trim_matches(['.', '_']);
if trimmed.is_empty() || trimmed == "." || trimmed == ".." {
"_".to_string()
} else {
trimmed.chars().take(200).collect()
}
}
fn lexically_clean(path: &Path) -> PathBuf {
let mut out = PathBuf::new();
for component in path.components() {
match component {
Component::ParentDir => {
out.pop();
}
Component::CurDir => {}
other => out.push(other.as_os_str()),
}
}
out
}
fn canonicalize_best_effort(path: &Path) -> PathBuf {
if let Ok(canon) = std::fs::canonicalize(path) {
return canon;
}
let mut remainder: Vec<std::ffi::OsString> = Vec::new();
let mut probe = path;
loop {
match probe.parent() {
Some(parent) => {
if let Some(name) = probe.file_name() {
remainder.push(name.to_os_string());
}
if let Ok(canon_parent) = std::fs::canonicalize(parent) {
let mut result = canon_parent;
for part in remainder.into_iter().rev() {
result.push(part);
}
return result;
}
probe = parent;
}
None => return lexically_clean(path),
}
}
}
fn fnv1a_64(bytes: &[u8]) -> u64 {
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash = FNV_OFFSET;
for byte in bytes {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
fn relocate_under_root(root: &Path, original: &Path) -> PathBuf {
let hash = fnv1a_64(original.as_os_str().as_encoded_bytes());
let short_hash = hash as u32;
let leaf = original
.file_name()
.and_then(|s| s.to_str())
.map(sanitize_path_component)
.filter(|s| s != "_");
let dir_name = match leaf {
Some(leaf) => format!("{leaf}-{short_hash:08x}"),
None => format!("relocated-{hash:x}"),
};
let target = root.join(dir_name);
if let Err(err) = std::fs::create_dir_all(&target) {
tracing::warn!(
path = %target.display(),
error = %err,
"relocate_under_root: failed to create relocated workspace directory"
);
}
target
}
pub fn pin_workspace_path(requested: &Path, root: &Path, confine: bool) -> PathBuf {
if !confine {
return requested.to_path_buf();
}
if let Err(err) = std::fs::create_dir_all(root) {
tracing::warn!(
path = %root.display(),
error = %err,
"pin_workspace_path: failed to create workspace root directory"
);
}
let canon_root = std::fs::canonicalize(root).unwrap_or_else(|_| root.to_path_buf());
let canon_requested = canonicalize_best_effort(requested);
if canon_requested.starts_with(&canon_root) {
return canon_requested;
}
relocate_under_root(&canon_root, requested)
}
pub fn default_session_workspace_dir(root: &Path, session_id: &str) -> PathBuf {
let dir = root.join(sanitize_path_component(session_id));
if let Err(err) = std::fs::create_dir_all(&dir) {
tracing::warn!(
path = %dir.display(),
error = %err,
"default_session_workspace_dir: failed to create session workspace directory"
);
}
dir
}
fn evict_oldest_if_needed(store: &DashMap<String, WorkspaceEntry>, max_tracked_workspaces: usize) {
if store.len() <= max_tracked_workspaces {
return;
}
let oldest = store
.iter()
.map(|entry| (entry.key().clone(), entry.value().last_touched))
.min_by_key(|(_, touched)| *touched);
if let Some((session_id, _)) = oldest {
let _ = store.remove(&session_id);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn evict_oldest_if_needed_removes_least_recent_entry() {
let store: DashMap<String, WorkspaceEntry> = DashMap::new();
let now = Instant::now();
store.insert(
"s1".to_string(),
WorkspaceEntry {
workspace: PathBuf::from("/tmp/s1"),
last_touched: now - Duration::from_secs(3),
},
);
store.insert(
"s2".to_string(),
WorkspaceEntry {
workspace: PathBuf::from("/tmp/s2"),
last_touched: now - Duration::from_secs(2),
},
);
store.insert(
"s3".to_string(),
WorkspaceEntry {
workspace: PathBuf::from("/tmp/s3"),
last_touched: now - Duration::from_secs(1),
},
);
evict_oldest_if_needed(&store, 2);
assert_eq!(store.len(), 2);
assert!(!store.contains_key("s1"));
assert!(store.contains_key("s2"));
assert!(store.contains_key("s3"));
}
#[test]
fn sanitize_path_component_allows_safe_chars_untouched() {
assert_eq!(
sanitize_path_component("session-123_abc.def"),
"session-123_abc.def"
);
}
#[test]
fn sanitize_path_component_replaces_separators_and_traversal() {
assert_eq!(sanitize_path_component("../../etc/passwd"), "etc_passwd");
assert_eq!(sanitize_path_component("a/b\\c"), "a_b_c");
}
#[test]
fn sanitize_path_component_rejects_dot_and_dotdot_and_empty() {
assert_eq!(sanitize_path_component("."), "_");
assert_eq!(sanitize_path_component(".."), "_");
assert_eq!(sanitize_path_component(""), "_");
assert_eq!(sanitize_path_component("///"), "_");
}
#[test]
fn sanitize_path_component_caps_length() {
let long = "a".repeat(500);
assert_eq!(sanitize_path_component(&long).len(), 200);
}
#[test]
fn default_session_workspace_dir_lands_under_root_and_is_created() {
let root_dir = tempfile::tempdir().unwrap();
let root = root_dir.path().join("workspaces");
let dir = default_session_workspace_dir(&root, "session-abc-123");
assert_eq!(dir, root.join("session-abc-123"));
assert!(dir.is_dir(), "default workspace dir should be created");
}
#[test]
fn default_session_workspace_dir_sanitizes_unsafe_session_ids() {
let root_dir = tempfile::tempdir().unwrap();
let root = root_dir.path().join("workspaces");
let dir = default_session_workspace_dir(&root, "../../etc");
assert_eq!(dir.parent().unwrap(), root);
assert!(dir.is_dir());
}
#[test]
fn pin_workspace_path_unconfined_allows_arbitrary_path_outside_root() {
let root_dir = tempfile::tempdir().unwrap();
let root = root_dir.path().join("workspaces");
let outside = tempfile::tempdir().unwrap();
let pinned = pin_workspace_path(outside.path(), &root, false);
assert_eq!(pinned, outside.path());
assert!(!pinned.starts_with(&root));
}
#[test]
fn pin_workspace_path_confined_keeps_path_already_inside_root() {
let root_dir = tempfile::tempdir().unwrap();
let root = root_dir.path().join("workspaces");
let inside = root.join("my-project");
std::fs::create_dir_all(&inside).unwrap();
let pinned = pin_workspace_path(&inside, &root, true);
assert_eq!(pinned, inside.canonicalize().unwrap());
assert!(pinned.starts_with(root.canonicalize().unwrap()));
}
#[test]
fn pin_workspace_path_confined_relocates_dotdot_escape() {
let root_dir = tempfile::tempdir().unwrap();
let root = root_dir.path().join("workspaces");
std::fs::create_dir_all(&root).unwrap();
let escape = root.join("../escaped-outside");
let pinned = pin_workspace_path(&escape, &root, true);
let canon_root = root.canonicalize().unwrap();
assert!(
pinned.starts_with(&canon_root),
"escaping `..` path must be relocated under root, got {pinned:?}"
);
let leaf = pinned.file_name().unwrap().to_str().unwrap();
assert!(
leaf.starts_with("escaped-outside-"),
"expected a hash-suffixed leaf, got {leaf:?}"
);
assert_eq!(pinned.parent().unwrap(), canon_root);
}
#[test]
fn pin_workspace_path_confined_relocates_absolute_escape() {
let root_dir = tempfile::tempdir().unwrap();
let root = root_dir.path().join("workspaces");
std::fs::create_dir_all(&root).unwrap();
let elsewhere = tempfile::tempdir().unwrap();
let absolute_escape = elsewhere.path().join("my-real-project");
std::fs::create_dir_all(&absolute_escape).unwrap();
let pinned = pin_workspace_path(&absolute_escape, &root, true);
let canon_root = root.canonicalize().unwrap();
assert!(
pinned.starts_with(&canon_root),
"escaping absolute path must be relocated under root, got {pinned:?}"
);
let leaf = pinned.file_name().unwrap().to_str().unwrap();
assert!(
leaf.starts_with("my-real-project-"),
"expected a hash-suffixed leaf, got {leaf:?}"
);
assert_eq!(pinned.parent().unwrap(), canon_root);
}
#[test]
fn pin_workspace_path_confined_relocates_same_basename_to_distinct_dirs() {
let root_dir = tempfile::tempdir().unwrap();
let root = root_dir.path().join("workspaces");
std::fs::create_dir_all(&root).unwrap();
let tenant_a = tempfile::tempdir().unwrap();
let tenant_b = tempfile::tempdir().unwrap();
let path_a = tenant_a.path().join("project");
let path_b = tenant_b.path().join("project");
std::fs::create_dir_all(&path_a).unwrap();
std::fs::create_dir_all(&path_b).unwrap();
let pinned_a = pin_workspace_path(&path_a, &root, true);
let pinned_b = pin_workspace_path(&path_b, &root, true);
assert_ne!(
pinned_a, pinned_b,
"escaping paths sharing a basename must relocate to distinct directories, \
got {pinned_a:?} and {pinned_b:?}"
);
let canon_root = root.canonicalize().unwrap();
assert!(pinned_a.starts_with(&canon_root));
assert!(pinned_b.starts_with(&canon_root));
}
#[test]
fn pin_workspace_path_confined_relocation_is_deterministic() {
let root_dir = tempfile::tempdir().unwrap();
let root = root_dir.path().join("workspaces");
std::fs::create_dir_all(&root).unwrap();
let elsewhere = tempfile::tempdir().unwrap();
let escape = elsewhere.path().join("repeatable-project");
std::fs::create_dir_all(&escape).unwrap();
let pinned_first = pin_workspace_path(&escape, &root, true);
let pinned_second = pin_workspace_path(&escape, &root, true);
assert_eq!(
pinned_first, pinned_second,
"pinning the same original path twice must produce the same relocated target"
);
}
#[test]
fn pin_workspace_path_confined_relocates_symlink_escape() {
let root_dir = tempfile::tempdir().unwrap();
let root = root_dir.path().join("workspaces");
std::fs::create_dir_all(&root).unwrap();
let outside = tempfile::tempdir().unwrap();
let outside_target = outside.path().join("secret");
std::fs::create_dir_all(&outside_target).unwrap();
let symlink_path = root.join("link-to-outside");
#[cfg(unix)]
std::os::unix::fs::symlink(&outside_target, &symlink_path).unwrap();
#[cfg(windows)]
std::os::windows::fs::symlink_dir(&outside_target, &symlink_path).unwrap();
let pinned = pin_workspace_path(&symlink_path, &root, true);
let canon_root = root.canonicalize().unwrap();
let canon_outside = outside_target.canonicalize().unwrap();
assert_ne!(
pinned, canon_outside,
"must not resolve to the symlink's real outside target"
);
assert!(
pinned.starts_with(&canon_root),
"symlink escape must be relocated under root, got {pinned:?}"
);
}
#[test]
fn workspace_or_process_cwd_falls_back_to_cwd_without_provider() {
assert!(!has_workspace_root_provider());
let session_id = format!("session_{}", uuid::Uuid::new_v4());
let resolved = workspace_or_process_cwd(Some(&session_id));
assert_eq!(resolved, std::env::current_dir().unwrap());
}
#[test]
fn workspace_or_process_cwd_without_session_id_falls_back_to_cwd() {
assert!(!has_workspace_root_provider());
assert_eq!(
workspace_or_process_cwd(None),
std::env::current_dir().unwrap()
);
}
}