use std::path::{Path, PathBuf};
pub struct RootIndex {
files: std::collections::HashMap<std::ffi::OsString, PathBuf>,
sorted_files: Vec<PathBuf>,
}
impl RootIndex {
pub fn build(project_dir: &Path) -> Self {
let mut files_by_name: std::collections::HashMap<std::ffi::OsString, PathBuf> =
std::collections::HashMap::new();
let mut sorted_files: Vec<PathBuf> = Vec::new();
if let Ok(rd) = std::fs::read_dir(project_dir) {
for entry in rd.flatten() {
let path = entry.path();
let is_file = entry
.file_type()
.map(|ft| ft.is_file())
.unwrap_or_else(|_| path.is_file());
if !is_file {
continue;
}
if let Some(name) = path.file_name() {
files_by_name.insert(name.to_owned(), path.clone());
}
sorted_files.push(path);
}
}
sorted_files.sort();
Self {
files: files_by_name,
sorted_files,
}
}
pub fn find_first_match(&self, pattern: &str) -> Option<&Path> {
if let Some(ext) = pattern.strip_prefix("*.") {
for path in &self.sorted_files {
if path.extension().and_then(|s| s.to_str()) == Some(ext) {
return Some(path);
}
}
return None;
}
self.files
.get(std::ffi::OsStr::new(pattern))
.map(|p| p.as_path())
}
}
#[cfg(test)]
pub fn find_first_match(project_dir: &Path, pattern: &str) -> Option<PathBuf> {
if let Some(ext) = pattern.strip_prefix("*.") {
let mut matches: Vec<std::path::PathBuf> = std::fs::read_dir(project_dir)
.ok()?
.flatten()
.map(|e| e.path())
.filter(|p| p.is_file())
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some(ext))
.collect();
matches.sort();
return matches.into_iter().next();
}
let candidate = project_dir.join(pattern);
if candidate.is_file() {
Some(candidate)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn exact_filename_match() {
let tmp = tempdir().unwrap();
fs::write(tmp.path().join("Cargo.toml"), "").unwrap();
assert!(find_first_match(tmp.path(), "Cargo.toml").is_some());
assert!(find_first_match(tmp.path(), "nope.txt").is_none());
}
#[test]
fn extension_glob_match() {
let tmp = tempdir().unwrap();
fs::write(tmp.path().join("main.tf"), "").unwrap();
let found = find_first_match(tmp.path(), "*.tf").unwrap();
assert_eq!(found.file_name().unwrap(), "main.tf");
}
#[test]
fn no_match_when_missing() {
let tmp = tempdir().unwrap();
assert!(find_first_match(tmp.path(), "Cargo.toml").is_none());
assert!(find_first_match(tmp.path(), "*.tf").is_none());
}
#[test]
fn extension_glob_returns_deterministic_match() {
let tmp = tempdir().unwrap();
for name in ["b.tf", "a.tf", "c.tf"] {
fs::write(tmp.path().join(name), "").unwrap();
}
let first = find_first_match(tmp.path(), "*.tf").unwrap();
assert_eq!(first.file_name().unwrap(), "a.tf");
for _ in 0..3 {
assert_eq!(
find_first_match(tmp.path(), "*.tf").unwrap(),
first,
"extension-glob matcher must be deterministic"
);
}
}
#[test]
fn does_not_walk_subdirs() {
let tmp = tempdir().unwrap();
fs::create_dir(tmp.path().join("sub")).unwrap();
fs::write(tmp.path().join("sub").join("Cargo.toml"), "").unwrap();
assert!(find_first_match(tmp.path(), "Cargo.toml").is_none());
}
#[test]
fn root_index_exact_lookup_matches_written_files() {
let tmp = tempdir().unwrap();
fs::write(tmp.path().join("Cargo.toml"), "").unwrap();
fs::write(tmp.path().join("package.json"), "").unwrap();
let idx = RootIndex::build(tmp.path());
assert!(idx.find_first_match("Cargo.toml").is_some());
assert!(idx.find_first_match("package.json").is_some());
assert!(idx.find_first_match("nonexistent.toml").is_none());
}
#[test]
fn root_index_filters_directories() {
let tmp = tempdir().unwrap();
fs::create_dir(tmp.path().join("subdir")).unwrap();
fs::write(tmp.path().join("Cargo.toml"), "").unwrap();
let idx = RootIndex::build(tmp.path());
assert!(
idx.find_first_match("subdir").is_none(),
"RootIndex must filter directories from the file map — \
`subdir` is a dir, not a file, so exact-name lookup must \
miss even though `read_dir` returned the entry"
);
assert!(idx.find_first_match("Cargo.toml").is_some());
}
#[test]
fn root_index_glob_is_deterministic_across_multiple_matches() {
let tmp = tempdir().unwrap();
for name in ["z.rockspec", "a.rockspec", "m.rockspec"] {
fs::write(tmp.path().join(name), "").unwrap();
}
let idx = RootIndex::build(tmp.path());
let first = idx.find_first_match("*.rockspec").unwrap();
assert_eq!(
first.file_name().unwrap(),
"a.rockspec",
"glob must return lexicographically-smallest match"
);
for _ in 0..3 {
assert_eq!(
idx.find_first_match("*.rockspec").unwrap(),
first,
"repeat calls must be deterministic (sort invariant)"
);
}
}
#[test]
fn root_index_empty_dir_yields_empty_matches() {
let tmp = tempdir().unwrap();
let idx = RootIndex::build(tmp.path());
assert!(idx.find_first_match("Cargo.toml").is_none());
assert!(idx.find_first_match("*.tf").is_none());
}
#[test]
fn root_index_only_dirs_yields_empty() {
let tmp = tempdir().unwrap();
for name in ["a", "b", "c"] {
fs::create_dir(tmp.path().join(name)).unwrap();
}
let idx = RootIndex::build(tmp.path());
assert!(
idx.find_first_match("a").is_none(),
"RootIndex indexes files only — a root of pure directories \
produces an empty index"
);
}
#[test]
fn root_index_missing_project_dir_yields_empty_index() {
let missing = std::path::Path::new("/this/does/not/exist/for/sure");
let idx = RootIndex::build(missing);
assert!(idx.find_first_match("Cargo.toml").is_none());
}
}