use crate::refresh::Sources;
use sha2::{Digest, Sha256};
use std::path::{Component, Path, PathBuf};
pub(crate) struct Places {
pub cwd: PathBuf,
pub home: PathBuf,
pub cache: PathBuf,
}
impl Places {
pub(crate) fn from_environment(cwd: &Path) -> Result<Self, String> {
let variable = |name: &str| {
std::env::var_os(name)
.filter(|value| !value.is_empty())
.map(PathBuf::from)
};
let (home, cache) = if cfg!(windows) {
let home = variable("USERPROFILE").ok_or("USERPROFILE is not set")?;
let local = variable("LOCALAPPDATA").ok_or("LOCALAPPDATA is not set")?;
(home, local.join("ripscope"))
} else {
let home = variable("HOME").ok_or("HOME is not set")?;
let cache = variable("XDG_CACHE_HOME")
.filter(|cache| cache.is_absolute())
.unwrap_or_else(|| home.join(".cache"));
(home, cache.join("ripscope"))
};
let cache = variable("RIPSCOPE_CACHE_DIR")
.filter(|cache| cache.is_absolute())
.unwrap_or(cache);
Ok(Self {
cwd: cwd.to_owned(),
home: absolute(&home, cwd),
cache: absolute(&cache, cwd),
})
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct Project {
pub root: PathBuf,
pub index: PathBuf,
pub selection: Vec<&'static str>,
}
impl Project {
pub(crate) fn sources(&self) -> Sources {
if self.selection.contains(&"-i") {
Sources::List(b"cscope.files".to_vec())
} else {
Sources::Automatic {
recursive: self.selection.contains(&"-R"),
}
}
}
}
pub(crate) fn find(path: &Path, places: &Places) -> Result<Project, String> {
let path = absolute(path, &places.cwd);
let start = if path.is_dir() {
path
} else {
path.parent().map_or(path.clone(), Path::to_owned)
};
let start = start
.ancestors()
.find(|directory| directory.is_dir())
.map_or(start.clone(), Path::to_owned);
let broad = |directory: &Path| same(directory, &places.home) || directory.parent().is_none();
let listed = start
.ancestors()
.find(|directory| directory.join("cscope.files").is_file())
.map(Path::to_owned);
let root = match &listed {
Some(listed) => listed.clone(),
None => {
let cwd = absolute(&places.cwd, &places.cwd);
let repository = start
.ancestors()
.find(|directory| directory.join(".git").exists() && !broad(directory));
match repository {
Some(repository) => repository.to_owned(),
None if inside(&start, &cwd)? && !broad(&cwd) => cwd,
None => start.clone(),
}
}
};
let selection = if listed.is_some() {
vec!["-k", "-i", "cscope.files"]
} else if broad(&root) {
vec!["-k"]
} else {
vec!["-k", "-R"]
};
let index = places.cache.join(index_name(&root)?);
Ok(Project {
root,
index,
selection,
})
}
pub(crate) fn index_name(root: &Path) -> Result<String, String> {
let name = root
.file_name()
.map(|name| {
name.to_string_lossy()
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || "._-".contains(c) {
c
} else {
'_'
}
})
.take(40)
.collect::<String>()
})
.filter(|name| !name.is_empty())
.unwrap_or_else(|| "root".to_owned());
let digest = Sha256::digest(key(root)?.as_bytes());
let hex = digest
.iter()
.take(8)
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
Ok(format!("{name}-{hex}.db"))
}
pub(crate) fn text(path: &Path) -> Result<String, String> {
let text = path
.to_str()
.ok_or_else(|| format!("{} is not valid Unicode", path.display()))?;
if cfg!(windows) {
let text = text.replace('\\', "/");
return Ok(if let Some(rest) = text.strip_prefix("//?/UNC/") {
format!("//{rest}")
} else if let Some(rest) = text.strip_prefix("//?/") {
rest.to_owned()
} else {
text
});
}
Ok(text.to_owned())
}
fn key(path: &Path) -> Result<String, String> {
let text = text(path)?;
Ok(if cfg!(windows) {
text.to_lowercase()
} else {
text
})
}
fn same(left: &Path, right: &Path) -> bool {
match (key(left), key(right)) {
(Ok(left), Ok(right)) => left == right,
_ => left == right,
}
}
fn inside(path: &Path, directory: &Path) -> Result<bool, String> {
let (path, directory) = (key(path)?, key(directory)?);
let prefix = format!("{}/", directory.trim_end_matches('/'));
Ok(path == directory || path.starts_with(&prefix))
}
pub(crate) fn absolute(path: &Path, cwd: &Path) -> PathBuf {
let joined = if path.is_absolute() {
path.to_owned()
} else {
cwd.join(path)
};
let mut resolved = PathBuf::new();
for component in joined.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
resolved.pop();
}
other => resolved.push(other.as_os_str()),
}
}
resolved
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
struct Fixture {
_temporary: tempfile::TempDir,
top: PathBuf,
places: Places,
}
fn fixture(cwd: &str) -> Fixture {
let temporary = tempfile::tempdir().unwrap();
let top = absolute(temporary.path(), Path::new("/"));
let home = top.join("home");
fs::create_dir_all(home.join(cwd)).unwrap();
Fixture {
places: Places {
cwd: home.join(cwd),
home: home.clone(),
cache: top.join("cache"),
},
top,
_temporary: temporary,
}
}
fn file(path: &Path) {
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, "int x;\n").unwrap();
}
#[test]
fn a_file_list_wins_over_a_repository_and_names_the_sources() {
let f = fixture("");
let root = f.places.home.join("kernel");
file(&root.join("cscope.files"));
fs::create_dir_all(root.join("drivers/net/.git")).unwrap();
let source = root.join("drivers/net/card.c");
file(&source);
let project = find(&source, &f.places).unwrap();
assert_eq!(project.root, root);
assert_eq!(project.selection, ["-k", "-i", "cscope.files"]);
assert_eq!(project.index.parent(), Some(f.places.cache.as_path()));
}
#[test]
fn a_repository_is_read_with_its_subdirectories() {
let f = fixture("elsewhere");
let root = f.places.home.join("src/tool");
fs::create_dir_all(root.join(".git")).unwrap();
let source = root.join("lib/deep/greet.c");
file(&source);
let project = find(&source, &f.places).unwrap();
assert_eq!((project.root, project.selection), (root, vec!["-k", "-R"]));
let worktree = f.places.home.join("worktree");
file(&worktree.join(".git"));
file(&worktree.join("a.c"));
assert_eq!(
find(&worktree.join("a.c"), &f.places).unwrap().root,
worktree
);
}
#[test]
fn a_home_repository_is_no_project() {
let f = fixture("");
fs::create_dir_all(f.places.home.join(".git")).unwrap();
let notes = f.places.home.join("notes");
file(¬es.join("scratch.c"));
let project = find(¬es.join("scratch.c"), &f.places).unwrap();
assert_eq!((project.root, project.selection), (notes, vec!["-k", "-R"]));
file(&f.places.home.join("top.c"));
let project = find(&f.places.home.join("top.c"), &f.places).unwrap();
assert_eq!(
(project.root, project.selection),
(f.places.home.clone(), vec!["-k"])
);
}
#[test]
fn an_unmarked_file_takes_the_current_directory_only_from_inside_it() {
let f = fixture("work/project");
let inside = f.places.cwd.join("src/main.c");
file(&inside);
assert_eq!(find(&inside, &f.places).unwrap().root, f.places.cwd);
let outside = f.top.join("tmp/stray.c");
file(&outside);
let project = find(&outside, &f.places).unwrap();
assert_eq!(project.root, f.top.join("tmp"));
assert_eq!(
find(&f.places.cwd.join("src"), &f.places).unwrap().root,
f.places.cwd
);
let new = f.top.join("tmp/fresh/deeper/new.c");
assert_eq!(find(&new, &f.places).unwrap().root, f.top.join("tmp"));
}
#[test]
fn relative_paths_and_dots_resolve_against_the_current_directory() {
let f = fixture("work");
let root = f.places.cwd.join("project");
fs::create_dir_all(root.join(".git")).unwrap();
file(&root.join("a.c"));
let project = find(Path::new("project/../project/./a.c"), &f.places).unwrap();
assert_eq!(project.root, root);
}
#[test]
fn index_names_keep_safe_characters_and_hash_the_whole_path() {
let root = if cfg!(windows) {
PathBuf::from(r"C:\Projects\My Café")
} else {
PathBuf::from("/home/me/My Café")
};
let key = if cfg!(windows) {
"c:/projects/my café"
} else {
"/home/me/My Café"
};
let digest = format!("{:x}", Sha256::digest(key.as_bytes()));
assert_eq!(
index_name(&root).unwrap(),
format!("My_Caf_-{}.db", &digest[..16])
);
let long = Path::new("/a").join("x".repeat(60));
assert_eq!(index_name(&long).unwrap().len(), 40 + 1 + 16 + 3);
let top = if cfg!(windows) { r"C:\" } else { "/" };
assert!(index_name(Path::new(top)).unwrap().starts_with("root-"));
}
#[cfg(windows)]
#[test]
fn windows_names_ignore_case_and_verbatim_prefixes() {
let hash = |path: &str| {
let name = index_name(Path::new(path)).unwrap();
name[name.len() - 19..].to_owned()
};
assert_eq!(hash(r"C:\Proj\ÄBC"), hash(r"c:\proj\äbc"));
assert_eq!(
index_name(Path::new(r"C:\Proj\Tool")).unwrap(),
index_name(Path::new(r"c:/PROJ/Tool")).unwrap()
);
assert_eq!(text(Path::new(r"\\?\C:\proj")).unwrap(), "C:/proj");
assert_eq!(
text(Path::new(r"\\?\UNC\server\share\proj")).unwrap(),
"//server/share/proj"
);
}
}