locate_dwarf/
lib.rs

1#![warn(clippy::all)]
2
3use anyhow::{anyhow, Error};
4use object::Object;
5use std::fmt::Write;
6use std::fs;
7use std::path::{Path, PathBuf};
8
9pub type Uuid = [u8; 16];
10
11mod pdb;
12use crate::pdb::locate_pdb;
13
14mod path_utils;
15use crate::path_utils::path_from_bytes;
16
17cfg_if::cfg_if! {
18    if #[cfg(target_os = "macos")] {
19        mod macos;
20        use crate::macos::locate_dsym_using_spotlight;
21    } else {
22        #[allow(clippy::unnecessary_wraps)]
23        fn locate_dsym_using_spotlight(_uuid: Uuid) -> Result<Option<PathBuf>, Error> {
24            Ok(None)
25        }
26    }
27}
28
29/// On macOS it can take some time for spotlight to index the dSYM file and on other OSes it is
30/// impossible to use spotlight. When built by cargo, we can likely find the dSYM file in
31/// target/<profile>/deps or target/<profile>/examples. Otherwise it can likely be found at
32/// <filename>.dSYM. This function will try to find it there.
33///
34/// # Arguments
35///
36/// * Parsed version of the object file which needs its debuginfo.
37/// * Path to the object file.
38fn locate_dsym_fastpath(path: &Path, uuid: Uuid) -> Option<PathBuf> {
39    // Canonicalize the path to make sure the fastpath also works when current working
40    // dir is inside target/
41    let path = path.canonicalize().ok()?;
42
43    // First try <path>.dSYM
44    let mut dsym = path.file_name()?.to_owned();
45    dsym.push(".dSYM");
46    let dsym_dir = path.with_file_name(&dsym);
47    if let Some(f) = try_match_dsym(&dsym_dir, uuid) {
48        return Some(f);
49    }
50
51    // Get the path to the target dir of the current build channel.
52    let mut target_channel_dir = &*path;
53    loop {
54        let parent = target_channel_dir.parent()?;
55        target_channel_dir = parent;
56
57        if target_channel_dir.parent().and_then(Path::file_name)
58            == Some(std::ffi::OsStr::new("target"))
59        {
60            break; // target_dir = ???/target/<channel>
61        }
62    }
63
64    // Check every entry in <target_channel_dir>/deps and <target_channel_dir>/examples
65    let deps_dir = target_channel_dir.join("deps");
66    let examples_dir = target_channel_dir.join("examples");
67    try_match_dsym_in_dir(&deps_dir, uuid).or_else(|| try_match_dsym_in_dir(&examples_dir, uuid))
68}
69
70fn try_match_dsym_in_dir(dir: &Path, uuid: Uuid) -> Option<PathBuf> {
71    for entry in fs::read_dir(dir).ok()? {
72        let item = entry.ok()?.path();
73
74        // If not a dSYM dir, try next entry.
75        if item.extension() != Some(std::ffi::OsStr::new("dSYM")) {
76            continue;
77        }
78
79        if let Some(debug_file_name) = try_match_dsym(&item, uuid) {
80            return Some(debug_file_name);
81        }
82    }
83
84    None
85}
86
87fn try_match_dsym(dsym_dir: &Path, uuid: Uuid) -> Option<PathBuf> {
88    // Get path to inner object file.
89    let mut dir_iter = fs::read_dir(dsym_dir.join("Contents/Resources/DWARF")).ok()?;
90
91    let debug_file_name = dir_iter.next()?.ok()?.path();
92
93    if dir_iter.next().is_some() {
94        return None; // There should only be one file in the `DWARF` directory.
95    }
96
97    // Parse inner object file.
98    let file = fs::read(&debug_file_name).ok()?;
99    let dsym = object::File::parse(&file[..]).ok()?;
100
101    // Make sure the dSYM file matches the object file to find debuginfo for.
102    if dsym.mach_uuid() == Ok(Some(uuid)) {
103        Some(debug_file_name)
104    } else {
105        None
106    }
107}
108
109/// Attempt to locate the path to separate debug symbols for `object` at `path`.
110///
111/// If `object` does not contain information that can be used to locate debug symbols for it,
112/// or if the debug symbol file is not present on disk, return None.
113///
114/// Currently only locating Mach-O dSYM bundles is supported.
115pub fn locate_debug_symbols<'a, O, T>(object: &'a O, path: T) -> Result<Option<PathBuf>, Error>
116where
117    O: Object<'a, 'a>,
118    T: AsRef<Path>,
119{
120    if let Some(uuid) = object.mach_uuid()? {
121        return locate_dsym(path.as_ref(), uuid);
122    }
123    if let Some(pdbinfo) = object.pdb_info()? {
124        return locate_pdb(path.as_ref(), &pdbinfo);
125    }
126    if let Some(path) = object
127        .build_id()?
128        .and_then(locate_debug_build_id)
129    {
130        return Ok(Some(path));
131        // If not found, try gnu_debuglink.
132    }
133    if let Some((filename, crc)) = object.gnu_debuglink()? {
134        let filename = path_from_bytes(filename)?;
135        return locate_gnu_debuglink(path.as_ref(), filename, crc);
136    }
137    Ok(None)
138}
139
140/// Attempt to locate the Mach-O file contained within a dSYM bundle containing the debug
141/// symbols for the Mach-O file at `path` with UUID `uuid`.
142pub fn locate_dsym<T>(path: T, uuid: Uuid) -> Result<Option<PathBuf>, Error>
143where
144    T: AsRef<Path>,
145{
146    if let Some(dsym_path) = locate_dsym_fastpath(path.as_ref(), uuid) {
147        return Ok(Some(dsym_path));
148    }
149    locate_dsym_using_spotlight(uuid)
150}
151
152/// Attempt to locate the separate debug symbol file for the object file at `path` with
153/// build ID `id`.
154pub fn locate_debug_build_id(id: &[u8]) -> Option<PathBuf> {
155    if id.len() < 2 {
156        return None;
157    }
158
159    // Try "/usr/lib/debug/.build-id/12/345678etc.debug"
160    let mut f = format!("/usr/lib/debug/.build-id/{:02x}/", id[0]);
161    for x in &id[1..] {
162        let _ = write!(&mut f, "{:02x}", x);
163    }
164    let _ = write!(&mut f, ".debug");
165    let f = PathBuf::from(f);
166    if f.exists() {
167        return Some(f);
168    }
169
170    None
171}
172
173/// Attempt to locate the separate debug symbol file for the object file at `path` with
174/// GNU "debug link" information consisting of `filename` and `crc`.
175pub fn locate_gnu_debuglink<T, U>(path: T, filename: U, _crc: u32) -> Result<Option<PathBuf>, Error>
176where
177    T: AsRef<Path>,
178    U: AsRef<Path>,
179{
180    let path = fs::canonicalize(path)?;
181    let parent = path.parent().ok_or_else(|| anyhow!("Bad path"))?;
182    let filename = filename.as_ref();
183
184    // TODO: check CRC
185
186    // Try "/parent/filename" if it differs from "path"
187    let f = parent.join(filename);
188    if f != path && f.exists() {
189        return Ok(Some(f));
190    }
191
192    // Try "/parent/.debug/filename"
193    let f = parent.join(".debug").join(filename);
194    if f.exists() {
195        return Ok(Some(f));
196    }
197
198    // Try "/usr/lib/debug/parent/filename"
199    let parent = parent.strip_prefix("/").unwrap();
200    let f = Path::new("/usr/lib/debug").join(parent).join(filename);
201    if f.exists() {
202        return Ok(Some(f));
203    }
204
205    Ok(None)
206}