use crate::config::settings::SourceConfig;
use ghostscope_ui::events::{PathSubstitution, SourcePathInfo};
use std::path::{Path, PathBuf};
use tracing::{debug, warn};
#[derive(Debug)]
pub struct SourcePathResolver {
config_substitutions: Vec<PathSubstitution>,
config_search_dirs: Vec<String>,
runtime_substitutions: Vec<PathSubstitution>,
runtime_search_dirs: Vec<String>,
}
impl SourcePathResolver {
pub fn new(config: &SourceConfig) -> Self {
let config_substitutions = config
.substitutions
.iter()
.map(Self::convert_substitution)
.collect();
Self {
config_substitutions,
config_search_dirs: config.search_dirs.clone(),
runtime_substitutions: Vec::new(),
runtime_search_dirs: Vec::new(),
}
}
#[inline]
fn convert_substitution(sub: &crate::config::settings::PathSubstitution) -> PathSubstitution {
PathSubstitution {
from: sub.from.clone(),
to: sub.to.clone(),
}
}
pub fn resolve(&self, dwarf_path: &str) -> Option<PathBuf> {
let path = Path::new(dwarf_path);
if path.exists() {
debug!("Source path resolved (exact): {}", dwarf_path);
return Some(path.to_path_buf());
}
if let Some(substituted) = self.try_substitute_path(dwarf_path) {
let new_path = PathBuf::from(&substituted);
if new_path.exists() {
debug!(
"Source path resolved (substitution): {} -> {}",
dwarf_path,
new_path.display()
);
return Some(new_path);
}
}
if let Some(basename) = path.file_name() {
for search_dir in self
.runtime_search_dirs
.iter()
.chain(self.config_search_dirs.iter())
{
let candidate = PathBuf::from(search_dir).join(basename);
if candidate.exists() {
debug!(
"Source path resolved (search dir): {} -> {} (dir: {})",
dwarf_path,
candidate.display(),
search_dir
);
return Some(candidate);
}
}
}
warn!("Failed to resolve source path: {}", dwarf_path);
None
}
pub fn add_search_dir(&mut self, dir: String) {
if !self.runtime_search_dirs.contains(&dir) {
self.runtime_search_dirs.push(dir);
}
}
pub fn add_substitution(&mut self, from: String, to: String) {
if let Some(existing) = self
.runtime_substitutions
.iter_mut()
.find(|s| s.from == from)
{
existing.to = to;
} else {
self.runtime_substitutions
.push(PathSubstitution { from, to });
}
}
pub fn remove(&mut self, pattern: &str) -> bool {
let mut removed = false;
if let Some(pos) = self.runtime_search_dirs.iter().position(|d| d == pattern) {
self.runtime_search_dirs.remove(pos);
removed = true;
}
if let Some(pos) = self
.runtime_substitutions
.iter()
.position(|s| s.from == pattern)
{
self.runtime_substitutions.remove(pos);
removed = true;
}
removed
}
pub fn clear_runtime(&mut self) {
self.runtime_substitutions.clear();
self.runtime_search_dirs.clear();
}
pub fn reset(&mut self) {
self.clear_runtime();
}
fn try_substitute_path(&self, path: &str) -> Option<String> {
for sub in self
.runtime_substitutions
.iter()
.chain(self.config_substitutions.iter())
{
if let Some(suffix) = path.strip_prefix(&sub.from) {
if suffix.is_empty() || suffix.starts_with('/') {
return Some(format!("{}{}", sub.to, suffix));
}
}
}
None
}
pub fn get_all_rules(&self) -> SourcePathInfo {
let all_substitutions: Vec<PathSubstitution> = self
.runtime_substitutions
.iter()
.chain(self.config_substitutions.iter())
.cloned()
.collect();
let all_search_dirs: Vec<String> = self
.runtime_search_dirs
.iter()
.chain(self.config_search_dirs.iter())
.cloned()
.collect();
SourcePathInfo {
substitutions: all_substitutions,
search_dirs: all_search_dirs,
runtime_substitution_count: self.runtime_substitutions.len(),
runtime_search_dir_count: self.runtime_search_dirs.len(),
config_substitution_count: self.config_substitutions.len(),
config_search_dir_count: self.config_search_dirs.len(),
}
}
pub fn reverse_map_to_dwarf(&self, fs_path: &str) -> Option<String> {
for sub in self
.runtime_substitutions
.iter()
.chain(self.config_substitutions.iter())
{
if let Some(suffix) = fs_path.strip_prefix(&sub.to) {
if suffix.is_empty() || suffix.starts_with('/') {
return Some(format!("{}{}", sub.from, suffix));
}
}
}
None
}
}
pub fn apply_substitutions_to_directory(resolver: &SourcePathResolver, directory: &str) -> String {
resolver
.try_substitute_path(directory)
.unwrap_or_else(|| directory.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::settings::{PathSubstitution as SettingsPathSubstitution, SourceConfig};
fn create_test_resolver(
config_subs: Vec<(&str, &str)>,
config_search: Vec<&str>,
) -> SourcePathResolver {
let config = SourceConfig {
substitutions: config_subs
.into_iter()
.map(|(from, to)| SettingsPathSubstitution {
from: from.to_string(),
to: to.to_string(),
})
.collect(),
search_dirs: config_search.into_iter().map(|s| s.to_string()).collect(),
};
SourcePathResolver::new(&config)
}
#[test]
fn test_boundary_matching_prevents_partial_matches() {
let resolver = create_test_resolver(vec![("/home/user", "/local/user")], vec![]);
let result = resolver.try_substitute_path("/home/username");
assert_eq!(result, None);
let result2 = resolver.try_substitute_path("/home/user2");
assert_eq!(result2, None);
let result3 = resolver.try_substitute_path("/home/user");
assert_eq!(result3, Some("/local/user".to_string()));
let result4 = resolver.try_substitute_path("/home/user/project/main.c");
assert_eq!(result4, Some("/local/user/project/main.c".to_string()));
}
#[test]
fn test_runtime_substitutions_override_config() {
let mut resolver = create_test_resolver(vec![("/build", "/config/path")], vec![]);
resolver.add_substitution("/build".to_string(), "/runtime/path".to_string());
let result = resolver.try_substitute_path("/build/main.c");
assert_eq!(result, Some("/runtime/path/main.c".to_string()));
}
#[test]
fn test_apply_substitutions_to_directory() {
let resolver = create_test_resolver(vec![("/usr/src/debug", "/home/user/sources")], vec![]);
let result = apply_substitutions_to_directory(&resolver, "/usr/src/debug/myproject");
assert_eq!(result, "/home/user/sources/myproject");
let result2 = apply_substitutions_to_directory(&resolver, "/usr/src/debug-backup");
assert_eq!(result2, "/usr/src/debug-backup");
let result3 = apply_substitutions_to_directory(&resolver, "/other/path");
assert_eq!(result3, "/other/path");
}
#[test]
fn test_search_dir_management() {
let mut resolver = create_test_resolver(vec![], vec!["/config/search"]);
resolver.add_search_dir("/runtime/search".to_string());
let rules = resolver.get_all_rules();
assert_eq!(rules.runtime_search_dir_count, 1);
assert_eq!(rules.config_search_dir_count, 1);
assert!(rules.search_dirs.contains(&"/runtime/search".to_string()));
let removed = resolver.remove("/runtime/search");
assert!(removed);
let rules2 = resolver.get_all_rules();
assert_eq!(rules2.runtime_search_dir_count, 0);
assert!(!rules2.search_dirs.contains(&"/runtime/search".to_string()));
}
#[test]
fn test_substitution_management() {
let mut resolver = create_test_resolver(vec![("/config", "/cfg")], vec![]);
resolver.add_substitution("/runtime".to_string(), "/rt".to_string());
let rules = resolver.get_all_rules();
assert_eq!(rules.runtime_substitution_count, 1);
assert_eq!(rules.config_substitution_count, 1);
let removed = resolver.remove("/runtime");
assert!(removed);
let rules2 = resolver.get_all_rules();
assert_eq!(rules2.runtime_substitution_count, 0);
assert_eq!(rules2.config_substitution_count, 1);
}
#[test]
fn test_clear_and_reset() {
let mut resolver = create_test_resolver(vec![("/config", "/cfg")], vec!["/config/dir"]);
resolver.add_substitution("/runtime".to_string(), "/rt".to_string());
resolver.add_search_dir("/runtime/dir".to_string());
resolver.clear_runtime();
let rules = resolver.get_all_rules();
assert_eq!(rules.runtime_substitution_count, 0);
assert_eq!(rules.runtime_search_dir_count, 0);
assert_eq!(rules.config_substitution_count, 1);
assert_eq!(rules.config_search_dir_count, 1);
resolver.add_substitution("/temp".to_string(), "/tmp".to_string());
resolver.reset();
let rules2 = resolver.get_all_rules();
assert_eq!(rules2.runtime_substitution_count, 0);
}
#[test]
fn test_duplicate_prevention() {
let mut resolver = create_test_resolver(vec![], vec![]);
resolver.add_substitution("/path".to_string(), "/new".to_string());
resolver.add_substitution("/path".to_string(), "/new".to_string());
let rules = resolver.get_all_rules();
assert_eq!(rules.runtime_substitution_count, 1);
resolver.add_search_dir("/search".to_string());
resolver.add_search_dir("/search".to_string());
let rules2 = resolver.get_all_rules();
assert_eq!(rules2.runtime_search_dir_count, 1);
}
#[test]
fn test_update_existing_substitution() {
let mut resolver = create_test_resolver(vec![], vec![]);
resolver.add_substitution("/build".to_string(), "/wrong/path".to_string());
let result = resolver.try_substitute_path("/build/main.c");
assert_eq!(result, Some("/wrong/path/main.c".to_string()));
let rules = resolver.get_all_rules();
assert_eq!(rules.runtime_substitution_count, 1);
resolver.add_substitution("/build".to_string(), "/correct/path".to_string());
let rules2 = resolver.get_all_rules();
assert_eq!(rules2.runtime_substitution_count, 1);
let result2 = resolver.try_substitute_path("/build/main.c");
assert_eq!(result2, Some("/correct/path/main.c".to_string()));
assert!(rules2
.substitutions
.iter()
.any(|s| s.from == "/build" && s.to == "/correct/path"));
assert!(!rules2
.substitutions
.iter()
.any(|s| s.from == "/build" && s.to == "/wrong/path"));
}
}