use std::path::{Component, Path, PathBuf};
pub fn normalize_path_components(path: &Path) -> Vec<String> {
path.components()
.filter_map(|component| match component {
Component::Normal(s) => Some(s.to_string_lossy().into_owned()),
Component::RootDir | Component::Prefix(_) => None,
Component::CurDir => None,
Component::ParentDir => None, })
.collect()
}
pub fn normalize_path_separators(path: &Path) -> String {
path.to_string_lossy()
.replace('\\', "/")
.trim_end_matches('/')
.trim_start_matches("./")
.to_string()
}
pub fn paths_match_by_suffix(query: &[String], target: &[String]) -> bool {
if query.is_empty() {
return false;
}
if query.len() > target.len() {
return false;
}
let target_suffix = &target[target.len() - query.len()..];
query == target_suffix
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatchStrategy {
ExactComponents,
QuerySuffix,
CandidateSuffix,
}
pub fn find_matching_path<'a>(
query_path: &Path,
available_paths: &'a [PathBuf],
) -> Option<(&'a PathBuf, MatchStrategy)> {
let query_components = normalize_path_components(query_path);
for candidate in available_paths {
let candidate_components = normalize_path_components(candidate);
if query_components == candidate_components {
return Some((candidate, MatchStrategy::ExactComponents));
}
}
for candidate in available_paths {
let candidate_components = normalize_path_components(candidate);
if paths_match_by_suffix(&query_components, &candidate_components) {
return Some((candidate, MatchStrategy::QuerySuffix));
}
}
for candidate in available_paths {
let candidate_components = normalize_path_components(candidate);
if paths_match_by_suffix(&candidate_components, &query_components) {
return Some((candidate, MatchStrategy::CandidateSuffix));
}
}
None
}
#[derive(Debug, Clone)]
pub struct NormalizedPath {
pub original: PathBuf,
pub components: Vec<String>,
pub normalized_str: String,
}
impl NormalizedPath {
pub fn from_path(path: &Path) -> Self {
Self {
original: path.to_path_buf(),
components: normalize_path_components(path),
normalized_str: normalize_path_separators(path),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_path_components_unix() {
assert_eq!(
normalize_path_components(Path::new("./src/lib.rs")),
vec!["src", "lib.rs"]
);
assert_eq!(
normalize_path_components(Path::new("/home/user/project/src/lib.rs")),
vec!["home", "user", "project", "src", "lib.rs"]
);
}
#[test]
fn test_normalize_path_components_relative() {
assert_eq!(
normalize_path_components(Path::new("src/lib.rs")),
vec!["src", "lib.rs"]
);
assert_eq!(
normalize_path_components(Path::new("./././src/lib.rs")),
vec!["src", "lib.rs"]
);
}
#[test]
fn test_normalize_path_components_empty() {
assert_eq!(
normalize_path_components(Path::new("")),
Vec::<String>::new()
);
assert_eq!(
normalize_path_components(Path::new(".")),
Vec::<String>::new()
);
assert_eq!(
normalize_path_components(Path::new("./")),
Vec::<String>::new()
);
}
#[test]
fn test_normalize_path_components_root() {
assert_eq!(
normalize_path_components(Path::new("/")),
Vec::<String>::new()
);
}
#[test]
fn test_normalize_path_separators_unix() {
assert_eq!(
normalize_path_separators(Path::new("src/lib.rs")),
"src/lib.rs"
);
assert_eq!(
normalize_path_separators(Path::new("/abs/path/src/lib.rs")),
"/abs/path/src/lib.rs"
);
}
#[test]
fn test_normalize_path_separators_backslash() {
assert_eq!(
normalize_path_separators(Path::new("src\\lib.rs")),
"src/lib.rs"
);
assert_eq!(
normalize_path_separators(Path::new("src\\utils\\helper.rs")),
"src/utils/helper.rs"
);
}
#[test]
fn test_normalize_path_separators_mixed() {
assert_eq!(
normalize_path_separators(Path::new("src/utils\\helper.rs")),
"src/utils/helper.rs"
);
}
#[test]
fn test_normalize_path_separators_trailing_slash() {
assert_eq!(
normalize_path_separators(Path::new("src/lib.rs/")),
"src/lib.rs"
);
assert_eq!(
normalize_path_separators(Path::new("src/lib.rs//")),
"src/lib.rs"
);
}
#[test]
fn test_paths_match_by_suffix() {
let query = vec!["src".to_string(), "lib.rs".to_string()];
let target = vec![
"project".to_string(),
"src".to_string(),
"lib.rs".to_string(),
];
assert!(paths_match_by_suffix(&query, &target));
let non_matching = vec!["other".to_string(), "lib.rs".to_string()];
assert!(!paths_match_by_suffix(&query, &non_matching));
}
#[test]
fn test_paths_match_by_suffix_exact() {
let query = vec!["src".to_string(), "lib.rs".to_string()];
let target = vec!["src".to_string(), "lib.rs".to_string()];
assert!(paths_match_by_suffix(&query, &target));
}
#[test]
fn test_paths_match_by_suffix_empty_query() {
let query: Vec<String> = vec![];
let target = vec!["src".to_string(), "lib.rs".to_string()];
assert!(!paths_match_by_suffix(&query, &target));
}
#[test]
fn test_paths_match_by_suffix_query_longer() {
let query = vec!["a".to_string(), "b".to_string(), "c".to_string()];
let target = vec!["b".to_string(), "c".to_string()];
assert!(!paths_match_by_suffix(&query, &target));
}
#[test]
fn test_find_matching_path_exact() {
let query = PathBuf::from("src/lib.rs");
let candidates = vec![PathBuf::from("src/lib.rs"), PathBuf::from("other/file.rs")];
let result = find_matching_path(&query, &candidates);
assert!(result.is_some());
let (matched, strategy) = result.unwrap();
assert_eq!(matched, &PathBuf::from("src/lib.rs"));
assert_eq!(strategy, MatchStrategy::ExactComponents);
}
#[test]
fn test_find_matching_path_query_suffix() {
let query = PathBuf::from("src/lib.rs");
let candidates = vec![
PathBuf::from("/abs/path/src/lib.rs"),
PathBuf::from("other/file.rs"),
];
let result = find_matching_path(&query, &candidates);
assert!(result.is_some());
let (matched, strategy) = result.unwrap();
assert_eq!(matched, &PathBuf::from("/abs/path/src/lib.rs"));
assert_eq!(strategy, MatchStrategy::QuerySuffix);
}
#[test]
fn test_find_matching_path_candidate_suffix() {
let query = PathBuf::from("/abs/path/src/lib.rs");
let candidates = vec![PathBuf::from("src/lib.rs"), PathBuf::from("other/file.rs")];
let result = find_matching_path(&query, &candidates);
assert!(result.is_some());
let (matched, strategy) = result.unwrap();
assert_eq!(matched, &PathBuf::from("src/lib.rs"));
assert_eq!(strategy, MatchStrategy::CandidateSuffix);
}
#[test]
fn test_find_matching_path_no_match() {
let query = PathBuf::from("src/lib.rs");
let candidates = vec![
PathBuf::from("other/file.rs"),
PathBuf::from("different/path.rs"),
];
let result = find_matching_path(&query, &candidates);
assert!(result.is_none());
}
#[test]
fn test_normalized_path_from_path() {
let normalized = NormalizedPath::from_path(Path::new("./src/lib.rs"));
assert_eq!(normalized.components, vec!["src", "lib.rs"]);
assert_eq!(normalized.normalized_str, "src/lib.rs"); assert_eq!(normalized.original, PathBuf::from("./src/lib.rs"));
}
#[test]
fn test_windows_unix_cross_platform_match() {
let lcov_path = PathBuf::from("C:/project/src/lib.rs");
let query_path = PathBuf::from("/home/dev/project/src/lib.rs");
let lcov_components = normalize_path_components(&lcov_path);
let query_components = normalize_path_components(&query_path);
let common_suffix = vec!["src".to_string(), "lib.rs".to_string()];
assert!(paths_match_by_suffix(&common_suffix, &lcov_components));
assert!(paths_match_by_suffix(&common_suffix, &query_components));
}
#[test]
fn test_docker_container_path_match() {
let container_path = PathBuf::from("/app/src/lib.rs");
let host_path = PathBuf::from("src/lib.rs");
let candidates = vec![container_path.clone()];
let result = find_matching_path(&host_path, &candidates);
assert!(result.is_some());
assert_eq!(result.unwrap().0, &container_path);
assert_eq!(result.unwrap().1, MatchStrategy::QuerySuffix);
}
#[test]
fn test_cargo_workspace_relative_paths() {
let workspace_root = PathBuf::from("crates/parser/src/lib.rs");
let member_relative = PathBuf::from("src/lib.rs");
let candidates = vec![workspace_root.clone()];
let result = find_matching_path(&member_relative, &candidates);
assert!(result.is_some());
assert_eq!(result.unwrap().0, &workspace_root);
}
}