use std::path::{Component, Path, PathBuf};
#[derive(Clone, Debug)]
pub struct PathLabel {
pub label: String,
pub path_display: String,
}
pub fn classify(
path: &Path,
src_roots: &[PathBuf],
path_rewrites: &[(PathBuf, PathBuf)],
cwd: Option<&Path>,
) -> PathLabel {
if let Some(cwd) = cwd {
if let Some(label) = classify_cwd(path, cwd) {
return label;
}
}
if let Some(label) = classify_stdlib(path, path_rewrites) {
return label;
}
if let Some(label) = classify_cargo_registry(path) {
return label;
}
if let Some(label) = classify_cargo_git(path) {
return label;
}
if let Some(label) = classify_workspace(path, src_roots) {
return label;
}
PathLabel {
label: "(unknown)".to_owned(),
path_display: path.display().to_string(),
}
}
fn classify_cwd(path: &Path, cwd: &Path) -> Option<PathLabel> {
let rel = path.strip_prefix(cwd).ok()?;
let pkg = enclosing_package_name(cwd, rel);
let label = pkg
.or_else(|| cwd.file_name().and_then(|n| n.to_str()).map(str::to_owned))
.unwrap_or_else(|| "workspace".to_owned());
Some(PathLabel {
label,
path_display: rel.display().to_string(),
})
}
fn classify_stdlib(path: &Path, rewrites: &[(PathBuf, PathBuf)]) -> Option<PathLabel> {
for (_, replacement) in rewrites {
if let Ok(rel) = path.strip_prefix(replacement) {
let mut comps = rel.components();
let crate_name = comps.next()?.as_os_str().to_str()?.to_owned();
let after_crate: PathBuf = comps.collect();
return Some(PathLabel {
label: format!("stdlib · {crate_name}"),
path_display: after_crate.display().to_string(),
});
}
}
None
}
fn classify_cargo_registry(path: &Path) -> Option<PathLabel> {
let cargo_home = cargo_home()?;
let registry = cargo_home.join("registry").join("src");
let rel = path.strip_prefix(®istry).ok()?;
let mut comps = rel.components();
let _index = comps.next()?;
let pkg_dir = comps.next()?.as_os_str().to_str()?;
let rest: PathBuf = comps.collect();
let (name, version) = split_name_version(pkg_dir);
let label = match version {
Some(v) => format!("{name} v{v}"),
None => name.to_owned(),
};
Some(PathLabel {
label,
path_display: rest.display().to_string(),
})
}
fn classify_cargo_git(path: &Path) -> Option<PathLabel> {
let cargo_home = cargo_home()?;
let checkouts = cargo_home.join("git").join("checkouts");
let rel = path.strip_prefix(&checkouts).ok()?;
let mut comps = rel.components();
let repo_dir = comps.next()?.as_os_str().to_str()?;
let _rev = comps.next();
let rest: PathBuf = comps.collect();
let repo = repo_dir
.rsplit_once('-')
.filter(|(_, hash)| hash.len() == 16 && hash.bytes().all(|b| b.is_ascii_hexdigit()))
.map(|(name, _)| name)
.unwrap_or(repo_dir);
Some(PathLabel {
label: format!("{repo} (git)"),
path_display: rest.display().to_string(),
})
}
fn classify_workspace(path: &Path, src_roots: &[PathBuf]) -> Option<PathLabel> {
for root in src_roots {
let rel = match path.strip_prefix(root) {
Ok(r) => r,
Err(_) => continue,
};
let pkg = enclosing_package_name(root, rel);
let label = pkg
.or_else(|| root.file_name().and_then(|n| n.to_str()).map(str::to_owned))
.unwrap_or_else(|| "workspace".to_owned());
return Some(PathLabel {
label,
path_display: rel.display().to_string(),
});
}
None
}
fn enclosing_package_name(workspace: &Path, rel: &Path) -> Option<String> {
let mut current = workspace.join(rel);
while current.pop() {
if !current.starts_with(workspace) {
return None;
}
let manifest = current.join("Cargo.toml");
if manifest.is_file() {
return read_package_name(&manifest);
}
}
None
}
fn read_package_name(manifest: &Path) -> Option<String> {
let contents = std::fs::read_to_string(manifest).ok()?;
let mut in_package = false;
for line in contents.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
in_package = trimmed == "[package]";
continue;
}
if !in_package {
continue;
}
let Some(rest) = trimmed.strip_prefix("name") else {
continue;
};
let after_eq = rest.trim_start().strip_prefix('=')?.trim_start();
let stripped = after_eq.strip_prefix('"')?;
let end = stripped.find('"')?;
return Some(stripped[..end].to_owned());
}
None
}
fn split_name_version(dir_name: &str) -> (&str, Option<&str>) {
if let Some((name, version)) = dir_name.rsplit_once('-') {
if version.starts_with(|c: char| c.is_ascii_digit()) {
return (name, Some(version));
}
}
(dir_name, None)
}
fn cargo_home() -> Option<PathBuf> {
if let Some(p) = std::env::var_os("CARGO_HOME") {
return Some(PathBuf::from(p));
}
dirs::home_dir().map(|h| h.join(".cargo"))
}
#[allow(dead_code)]
pub fn canonicalize(path: &Path) -> PathBuf {
std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}
#[allow(dead_code)]
pub fn is_src_dir(c: Component<'_>) -> bool {
c.as_os_str() == "src"
}