use std::path::{Path, PathBuf};
use crate::service::colocated_storage::COLOCATED_DIR_NAME;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ColocatedIndexEntry {
pub root_path: PathBuf,
pub id: String,
}
pub fn id_from_path(path: &Path) -> String {
let raw = path
.to_string_lossy()
.trim_start_matches('/')
.trim_start_matches('\\')
.to_string();
let safe: String = raw
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' {
c
} else {
'_'
}
})
.collect();
safe.chars().take(200).collect()
}
pub fn scan_roots_for_colocated_indexes(
tracked_roots: &[PathBuf],
max_depth: usize,
) -> Vec<ColocatedIndexEntry> {
let mut seen: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();
let mut results = Vec::new();
for root in tracked_roots {
let canonical_root = match root.canonicalize() {
Ok(c) => c,
Err(e) => {
tracing::debug!(
"fs_discovery: skipping root {} (canonicalize: {e})",
root.display()
);
continue;
}
};
scan_dir_recursive(
&canonical_root,
&canonical_root,
0,
max_depth,
&mut seen,
&mut results,
);
}
results
}
fn scan_dir_recursive(
dir: &Path,
original_root: &Path,
depth: usize,
max_depth: usize,
seen: &mut std::collections::HashSet<PathBuf>,
results: &mut Vec<ColocatedIndexEntry>,
) {
let ts_dir = dir.join(COLOCATED_DIR_NAME);
if ts_dir.exists() && ts_dir.is_dir() {
let root_path = dir.to_path_buf();
if seen.insert(root_path.clone()) {
let id = id_from_path(&root_path);
tracing::debug!(
"fs_discovery: found colocated index at {} (id={id})",
root_path.display()
);
results.push(ColocatedIndexEntry { root_path, id });
}
}
if depth >= max_depth {
return;
}
let read_dir = match std::fs::read_dir(dir) {
Ok(rd) => rd,
Err(e) => {
tracing::debug!(
"fs_discovery: cannot read dir {} ({e}) — skipping",
dir.display()
);
return;
}
};
for entry in read_dir.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if name == COLOCATED_DIR_NAME || name == ".git" || name == "node_modules" {
continue;
}
let canonical_path = match path.canonicalize() {
Ok(c) => c,
Err(_) => continue,
};
if !canonical_path.starts_with(original_root) {
continue;
}
scan_dir_recursive(
&canonical_path,
original_root,
depth + 1,
max_depth,
seen,
results,
);
}
}
pub const DEFAULT_SCAN_DEPTH: usize = 5;
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
fn make_colocated(root: &Path) {
let ts = root.join(".trusty-search");
fs::create_dir_all(&ts).unwrap();
}
#[test]
fn id_from_path_is_stable_and_safe() {
let id = id_from_path(Path::new("/Users/bob/Projects/my-project"));
assert!(!id.contains('/'), "id must not contain /");
assert!(!id.contains(' '), "id must not contain spaces");
assert_eq!(
id,
id_from_path(Path::new("/Users/bob/Projects/my-project"))
);
}
#[test]
fn discovery_finds_root_and_nested() {
let tmp = tempdir().unwrap();
let root = tmp.path().to_path_buf();
make_colocated(&root);
let nested = root.join("services").join("api");
fs::create_dir_all(&nested).unwrap();
make_colocated(&nested);
let roots = vec![root.clone()];
let found = scan_roots_for_colocated_indexes(&roots, DEFAULT_SCAN_DEPTH);
let root_paths: Vec<_> = found.iter().map(|e| &e.root_path).collect();
let canon_root = root.canonicalize().unwrap();
let canon_nested = nested.canonicalize().unwrap();
assert!(
root_paths.contains(&&canon_root),
"top-level root must be found; got: {root_paths:?}"
);
assert!(
root_paths.contains(&&canon_nested),
"nested project must be found; got: {root_paths:?}"
);
assert_eq!(found.len(), 2, "exactly two indexes must be found");
}
#[test]
fn discovery_skips_missing_root() {
let nonexistent = PathBuf::from("/tmp/trusty-test-definitely-does-not-exist-xyz123");
let found = scan_roots_for_colocated_indexes(&[nonexistent], DEFAULT_SCAN_DEPTH);
assert!(found.is_empty(), "missing root must produce no results");
}
#[test]
fn discovery_dedupes_by_root_path() {
let tmp = tempdir().unwrap();
let root = tmp.path().to_path_buf();
make_colocated(&root);
let roots = vec![root.clone(), root.clone()];
let found = scan_roots_for_colocated_indexes(&roots, DEFAULT_SCAN_DEPTH);
assert_eq!(
found.len(),
1,
"duplicate root must not produce duplicate entries"
);
}
#[test]
fn discovery_does_not_descend_into_trusty_search() {
let tmp = tempdir().unwrap();
let root = tmp.path().to_path_buf();
make_colocated(&root);
let nested_ts = root.join(".trusty-search").join(".trusty-search");
fs::create_dir_all(&nested_ts).unwrap();
let found = scan_roots_for_colocated_indexes(&[root], DEFAULT_SCAN_DEPTH);
assert_eq!(
found.len(),
1,
"inner .trusty-search must not be discovered"
);
}
}