ghostscope_dwarf/analyzer/
module_resolution.rs1use super::{AddressQueryResult, DwarfAnalyzer};
2use crate::core::{ModuleAddress, Result};
3use std::ffi::OsStr;
4use std::os::unix::fs::MetadataExt;
5use std::path::{Component, Path, PathBuf};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ModuleDefaultPolicy {
9 MainExecutableOnly,
10 MainExecutableOrSingleSharedLibrary,
11}
12
13impl DwarfAnalyzer {
14 pub fn module_paths(&self) -> Vec<PathBuf> {
16 let mut modules: Vec<PathBuf> = self.modules.keys().cloned().collect();
17 modules.sort();
18 modules
19 }
20
21 pub fn module_paths_equivalent<P: AsRef<Path>, Q: AsRef<Path>>(left: P, right: Q) -> bool {
24 let left = left.as_ref();
25 let right = right.as_ref();
26 if left == right {
27 return true;
28 }
29
30 if files_have_same_identity(left, right) {
31 return true;
32 }
33
34 if has_proc_root_prefix(left) || has_proc_root_prefix(right) {
35 return false;
36 }
37
38 match (left.canonicalize(), right.canonicalize()) {
39 (Ok(left), Ok(right)) => left == right,
40 _ => false,
41 }
42 }
43
44 pub fn module_spec_matches_path<P: AsRef<Path>>(module_path: P, module_spec: &str) -> bool {
47 let spec = module_spec.trim();
48 if spec.is_empty() {
49 return false;
50 }
51
52 let module_path = module_path.as_ref();
53 Self::module_paths_equivalent(module_path, Path::new(spec))
54 || module_path.to_string_lossy().ends_with(spec)
55 }
56
57 pub fn resolve_loaded_module_by_spec(&self, module_spec: &str) -> Result<PathBuf> {
59 let module_spec = module_spec.trim();
60 if module_spec.is_empty() {
61 return Err(anyhow::anyhow!("Module spec is empty"));
62 }
63
64 let modules = self.module_paths();
65
66 if let Some(found) = modules
67 .iter()
68 .find(|module_path| Self::module_paths_equivalent(module_path, Path::new(module_spec)))
69 {
70 return Ok(found.clone());
71 }
72
73 let candidates: Vec<PathBuf> = modules
74 .into_iter()
75 .filter(|module_path| module_path.to_string_lossy().ends_with(module_spec))
76 .collect();
77
78 match candidates.len() {
79 0 => Err(anyhow::anyhow!(
80 "Module '{module_spec}' not found among loaded modules. Use full path or a unique suffix."
81 )),
82 1 => Ok(candidates[0].clone()),
83 _ => {
84 let sample: Vec<String> = candidates
85 .iter()
86 .take(5)
87 .map(|path| path.to_string_lossy().to_string())
88 .collect();
89 Err(anyhow::anyhow!(
90 "Ambiguous module suffix '{}'. Candidates:\n - {}\nPlease use a more specific suffix or full path.",
91 module_spec,
92 sample.join("\n - ")
93 ))
94 }
95 }
96 }
97
98 pub fn resolve_target_module_path(&self, target_path: &str) -> Result<PathBuf> {
100 let target_path = target_path.trim();
101 if target_path.is_empty() {
102 return Err(anyhow::anyhow!("Target path from -t is empty"));
103 }
104
105 let matches: Vec<PathBuf> = self
106 .module_paths()
107 .into_iter()
108 .filter(|module_path| {
109 Self::module_paths_equivalent(module_path, Path::new(target_path))
110 })
111 .collect();
112
113 match matches.len() {
114 0 => Err(anyhow::anyhow!(
115 "Target '{target_path}' from -t is not loaded in the analyzed modules. When -t and -p are combined, -t scopes trace target resolution and -p only supplies PID filtering."
116 )),
117 1 => Ok(matches[0].clone()),
118 _ => Err(anyhow::anyhow!(
119 "Target '{target_path}' from -t matches multiple loaded modules; use a more specific path."
120 )),
121 }
122 }
123
124 pub fn resolve_module_spec(
126 &self,
127 module_spec: &str,
128 target_path: Option<&str>,
129 ) -> Result<PathBuf> {
130 let Some(target_path) = target_path else {
131 return self.resolve_loaded_module_by_spec(module_spec);
132 };
133
134 let target_module = self.resolve_target_module_path(target_path)?;
135 if Self::module_spec_matches_path(&target_module, module_spec)
136 || Self::module_spec_matches_path(target_path, module_spec)
137 {
138 Ok(target_module)
139 } else {
140 Err(anyhow::anyhow!(
141 "Module '{}' is outside -t target '{}'. When -t is configured, module resolution is scoped to the target.",
142 module_spec.trim(),
143 target_module.display()
144 ))
145 }
146 }
147
148 pub fn resolve_address_module(
150 &self,
151 module_spec: Option<&str>,
152 target_path: Option<&str>,
153 fallback: ModuleDefaultPolicy,
154 ) -> Result<PathBuf> {
155 if let Some(module_spec) = module_spec {
156 return self.resolve_module_spec(module_spec, target_path);
157 }
158
159 if let Some(target_path) = target_path {
160 return self.resolve_target_module_path(target_path);
161 }
162
163 if let Some(main) = self
164 .module_paths()
165 .into_iter()
166 .find(|module_path| self.is_main_executable_module(module_path))
167 {
168 return Ok(main);
169 }
170
171 if fallback == ModuleDefaultPolicy::MainExecutableOrSingleSharedLibrary {
172 let libs: Vec<PathBuf> = self
173 .module_paths()
174 .into_iter()
175 .filter(|module_path| self.is_shared_library(module_path))
176 .collect();
177 if libs.len() == 1 {
178 return Ok(libs[0].clone());
179 }
180 }
181
182 let message = match fallback {
183 ModuleDefaultPolicy::MainExecutableOnly => {
184 "No default module available. Start with -p <pid> or -t <binary>."
185 }
186 ModuleDefaultPolicy::MainExecutableOrSingleSharedLibrary => {
187 "No module available to resolve address. In PID mode, default module is the main executable. In target mode (-t <binary>), the specified binary is used (including .so)."
188 }
189 };
190 Err(anyhow::anyhow!(message))
191 }
192
193 pub fn filter_module_addresses_to_target(
195 &self,
196 module_addresses: Vec<ModuleAddress>,
197 target_path: Option<&str>,
198 ) -> Result<Vec<ModuleAddress>> {
199 let Some(target_path) = target_path else {
200 return Ok(module_addresses);
201 };
202 if module_addresses.is_empty() {
203 return Ok(module_addresses);
204 }
205
206 let target_module = self.resolve_target_module_path(target_path)?;
207 Ok(module_addresses
208 .into_iter()
209 .filter(|module_address| {
210 Self::module_paths_equivalent(&module_address.module_path, &target_module)
211 })
212 .collect())
213 }
214
215 pub fn filter_address_results_to_target(
217 &self,
218 addresses: Vec<AddressQueryResult>,
219 target_path: Option<&str>,
220 ) -> Result<Vec<AddressQueryResult>> {
221 let Some(target_path) = target_path else {
222 return Ok(addresses);
223 };
224 if addresses.is_empty() {
225 return Ok(addresses);
226 }
227
228 let target_module = self.resolve_target_module_path(target_path)?;
229 Ok(addresses
230 .into_iter()
231 .filter(|address| Self::module_paths_equivalent(&address.module_path, &target_module))
232 .collect())
233 }
234}
235
236fn files_have_same_identity(left: &Path, right: &Path) -> bool {
237 match (std::fs::metadata(left), std::fs::metadata(right)) {
238 (Ok(left), Ok(right)) => left.dev() == right.dev() && left.ino() == right.ino(),
239 _ => false,
240 }
241}
242
243fn has_proc_root_prefix(path: &Path) -> bool {
244 strip_proc_root_prefix(path).is_some()
245}
246
247fn strip_proc_root_prefix(path: &Path) -> Option<PathBuf> {
248 let mut components = path.components();
249 if !matches!(components.next(), Some(Component::RootDir)) {
250 return None;
251 }
252 if !matches!(
253 components.next(),
254 Some(Component::Normal(component)) if component == OsStr::new("proc")
255 ) {
256 return None;
257 }
258 if !matches!(
259 components.next(),
260 Some(Component::Normal(pid)) if pid.as_encoded_bytes().iter().all(u8::is_ascii_digit)
261 ) {
262 return None;
263 }
264 if !matches!(
265 components.next(),
266 Some(Component::Normal(component)) if component == OsStr::new("root")
267 ) {
268 return None;
269 }
270
271 let remaining = components.as_path();
272 let mut stripped = PathBuf::from("/");
273 if !remaining.as_os_str().is_empty() {
274 stripped.push(remaining);
275 }
276 Some(stripped)
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn module_spec_rejects_empty_strings() {
285 assert!(!DwarfAnalyzer::module_spec_matches_path(
286 "/tmp/libfoo.so",
287 ""
288 ));
289 assert!(!DwarfAnalyzer::module_spec_matches_path(
290 "/tmp/libfoo.so",
291 " "
292 ));
293 }
294
295 #[test]
296 fn module_spec_matches_unique_suffixes() {
297 assert!(DwarfAnalyzer::module_spec_matches_path(
298 "/opt/app/lib/libfoo.so.1",
299 "lib/libfoo.so.1"
300 ));
301 assert!(DwarfAnalyzer::module_spec_matches_path(
302 "/opt/app/lib/libfoo.so.1",
303 "libfoo.so.1"
304 ));
305 }
306
307 #[test]
308 fn proc_root_paths_do_not_match_by_stripped_text() {
309 assert!(!DwarfAnalyzer::module_paths_equivalent(
310 "/proc/123/root/usr/lib/libfoo.so",
311 "/proc/456/root/usr/lib/libfoo.so"
312 ));
313 assert!(!DwarfAnalyzer::module_paths_equivalent(
314 "/proc/123/root/usr/lib/libfoo.so",
315 "/usr/lib/libfoo.so"
316 ));
317 }
318
319 #[test]
320 fn proc_root_paths_reject_non_pid_prefixes() {
321 assert!(!has_proc_root_prefix(Path::new(
322 "/proc/self/root/usr/lib/libfoo.so"
323 )));
324 assert!(!has_proc_root_prefix(Path::new("/proc/123/maps")));
325 }
326
327 #[cfg(unix)]
328 #[test]
329 fn proc_root_paths_match_by_file_identity() -> anyhow::Result<()> {
330 let temp_dir = tempfile::tempdir()?;
331 let real_path = temp_dir.path().join("libfoo.so");
332 std::fs::write(&real_path, b"test")?;
333 let proc_root_path = PathBuf::from(format!("/proc/{}/root", std::process::id()))
334 .join(real_path.strip_prefix("/")?);
335
336 assert!(DwarfAnalyzer::module_paths_equivalent(
337 &real_path,
338 &proc_root_path
339 ));
340
341 Ok(())
342 }
343
344 #[cfg(unix)]
345 #[test]
346 fn module_spec_matches_symlink_alias() -> anyhow::Result<()> {
347 use std::os::unix::fs as unix_fs;
348
349 let temp_dir = tempfile::tempdir()?;
350 let real_path = temp_dir.path().join("libfoo.so.1");
351 let alias_path = temp_dir.path().join("libfoo.so");
352 std::fs::write(&real_path, b"test")?;
353 unix_fs::symlink(&real_path, &alias_path)?;
354
355 assert!(DwarfAnalyzer::module_spec_matches_path(
356 &real_path,
357 alias_path.to_string_lossy().as_ref()
358 ));
359
360 Ok(())
361 }
362}