use std::path::{Path, PathBuf};
use rusqlite::{Connection, OpenFlags};
use reliary_search::schema::{MIN_MIGRATABLE, SCHEMA_VERSION};
pub fn db_path_for(root: &Path) -> PathBuf {
root.join(".reliary").join("index.sqlite")
}
pub fn is_reliary_index(db_path: &Path) -> bool {
if !db_path.is_file() {
return false;
}
match Connection::open_with_flags(db_path, OpenFlags::SQLITE_OPEN_READ_ONLY) {
Ok(db) => db
.query_row("PRAGMA user_version", [], |r| r.get::<_, i32>(0))
.map(|v| (MIN_MIGRATABLE..=SCHEMA_VERSION).contains(&v))
.unwrap_or(false),
Err(_) => false,
}
}
fn lexical_normalize(p: &Path) -> PathBuf {
use std::path::Component;
let mut out = PathBuf::new();
for c in p.components() {
match c {
Component::CurDir => {}
Component::ParentDir => {
match out.components().next_back() {
Some(Component::Normal(_)) => { out.pop(); }
Some(Component::RootDir) | Some(Component::Prefix(_)) => {}
_ => out.push(".."),
}
}
other => out.push(other.as_os_str()),
}
}
if out.as_os_str().is_empty() { PathBuf::from(".") } else { out }
}
pub fn nearest_index_root(start: &Path) -> Option<PathBuf> {
let base = lexical_normalize(&if start.is_absolute() {
start.to_path_buf()
} else {
std::env::current_dir().ok()?.join(start)
});
let mut cur = if base.is_dir() {
base
} else {
base.parent()?.to_path_buf()
};
loop {
if is_reliary_index(&db_path_for(&cur)) {
return Some(cur);
}
if !cur.pop() {
return None;
}
}
}
pub enum Resolution {
Found(PathBuf),
NotFound(PathBuf),
}
pub fn resolve(path_hint: Option<&str>, client_root: Option<&Path>) -> Resolution {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
if let Some(h) = path_hint.filter(|h| !h.is_empty()) {
let p = lexical_normalize(&cwd.join(h));
if p.is_dir() {
return match nearest_index_root(&p) {
Some(root) => Resolution::Found(root),
None => Resolution::NotFound(p),
};
}
}
if let Some(env_root) = std::env::var_os("RELIARY_ROOT") {
let p = PathBuf::from(env_root);
if p.is_dir() {
if let Some(root) = nearest_index_root(&p) {
return Resolution::Found(root);
}
}
}
if let Some(cr) = client_root.filter(|p| p.is_dir()) {
if let Some(root) = nearest_index_root(cr) {
return Resolution::Found(root);
}
}
match nearest_index_root(&cwd) {
Some(root) => Resolution::Found(root),
None => Resolution::NotFound(cwd),
}
}
pub fn searched_note(start: &Path) -> String {
format!(
"no index found at or above {} — run `reliary trust {}` to build one",
start.display(),
start.display()
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn make_index(root: &Path, version: i32) {
let dir = root.join(".reliary");
fs::create_dir_all(&dir).unwrap();
let db = Connection::open(dir.join("index.sqlite")).unwrap();
db.execute_batch(&format!("PRAGMA user_version = {}", version))
.unwrap();
}
#[test]
fn dot_hint_resolves_without_a_dot_component() {
let tmp = std::env::temp_dir().join(format!("relroot_dot_{}", std::process::id()));
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
make_index(&tmp, SCHEMA_VERSION);
let cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(&tmp).unwrap();
let res = resolve(Some("."), None);
std::env::set_current_dir(cwd).unwrap();
match res {
Resolution::Found(root) => {
assert!(
!root.components().any(|c| c == std::path::Component::CurDir),
"resolved root must not contain `.`: {root:?}"
);
assert_eq!(root, tmp);
}
Resolution::NotFound(p) => panic!("expected Found, got NotFound({p:?})"),
}
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn lexical_normalize_strips_dot_and_resolves_dotdot() {
assert_eq!(lexical_normalize(Path::new("/a/./b")), PathBuf::from("/a/b"));
assert_eq!(lexical_normalize(Path::new("/a/b/../c")), PathBuf::from("/a/c"));
assert_eq!(lexical_normalize(Path::new(".")), PathBuf::from("."));
assert_eq!(lexical_normalize(Path::new("/..")), PathBuf::from("/"));
}
#[test]
fn finds_index_in_same_dir() {
let tmp = std::env::temp_dir().join(format!("relroot_same_{}", std::process::id()));
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
make_index(&tmp, SCHEMA_VERSION);
assert_eq!(nearest_index_root(&tmp), Some(tmp.clone()));
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn walks_up_from_subdirectory() {
let tmp = std::env::temp_dir().join(format!("relroot_up_{}", std::process::id()));
let _ = fs::remove_dir_all(&tmp);
let deep = tmp.join("crates").join("agent").join("src");
fs::create_dir_all(&deep).unwrap();
make_index(&tmp, SCHEMA_VERSION);
assert_eq!(nearest_index_root(&deep), Some(tmp.clone()));
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn skips_stub_index_and_finds_real_one_above() {
let tmp = std::env::temp_dir().join(format!("relroot_stub_{}", std::process::id()));
let _ = fs::remove_dir_all(&tmp);
let inner = tmp.join("project");
fs::create_dir_all(inner.join(".reliary")).unwrap();
Connection::open(inner.join(".reliary").join("index.sqlite")).unwrap();
assert!(
!is_reliary_index(&inner.join(".reliary").join("index.sqlite")),
"a 0-version stub must not count as an index"
);
make_index(&tmp, SCHEMA_VERSION);
assert_eq!(nearest_index_root(&inner), Some(tmp.clone()));
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn path_hint_selects_project() {
let tmp = std::env::temp_dir().join(format!("relroot_hint_{}", std::process::id()));
let _ = fs::remove_dir_all(&tmp);
let proj = tmp.join("proj");
fs::create_dir_all(&proj).unwrap();
make_index(&proj, SCHEMA_VERSION);
let hint = proj.to_string_lossy().to_string();
match resolve(Some(&hint), None) {
Resolution::Found(r) => assert_eq!(r, proj),
Resolution::NotFound(_) => panic!("path hint should have resolved"),
}
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn client_root_used_when_no_hint() {
let tmp = std::env::temp_dir().join(format!("relroot_client_{}", std::process::id()));
let _ = fs::remove_dir_all(&tmp);
let proj = tmp.join("proj");
fs::create_dir_all(&proj).unwrap();
make_index(&proj, SCHEMA_VERSION);
match resolve(None, Some(&proj)) {
Resolution::Found(r) => assert_eq!(r, proj),
Resolution::NotFound(_) => panic!("client root should have resolved"),
}
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn read_only_probe_does_not_create_file() {
let tmp = std::env::temp_dir().join(format!("relroot_nocreate_{}", std::process::id()));
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
let missing = db_path_for(&tmp);
assert!(!is_reliary_index(&missing));
assert!(!missing.exists(), "is_reliary_index must never create the file");
let _ = fs::remove_dir_all(&tmp);
}
}