rudy_dwarf/file/
mod.rs

1use std::{
2    fmt::{self, Debug},
3    path::PathBuf,
4    sync::Arc,
5};
6
7use memmap2::Mmap;
8use object::{read::archive::ArchiveFile, Object};
9
10pub(crate) mod index;
11pub(crate) mod loader;
12
13pub use index::index_debug_file_sources;
14pub(crate) use loader::{Dwarf, DwarfReader, RawDie};
15
16use crate::DwarfDb;
17
18pub type Expression = gimli::Expression<DwarfReader>;
19
20#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, salsa::Update)]
21pub struct SourceLocation {
22    pub file: SourceFile,
23    pub line: u64,
24    pub column: Option<u64>,
25}
26
27impl SourceLocation {
28    pub fn new(file: SourceFile, line: u64, column: Option<u64>) -> Self {
29        Self { file, line, column }
30    }
31}
32
33pub fn detect_cargo_root() -> Option<PathBuf> {
34    fn cargo_detect_workspace() -> Option<PathBuf> {
35        let output = std::process::Command::new(env!("CARGO"))
36            .arg("locate-project")
37            .arg("--workspace")
38            .arg("--message-format=plain")
39            .output()
40            .ok()?
41            .stdout;
42        let cargo_path = PathBuf::from(std::str::from_utf8(&output).ok()?.trim());
43        Some(cargo_path.parent()?.to_path_buf())
44    }
45
46    fn cwd_detect_workspace() -> Option<PathBuf> {
47        let mut path = std::env::current_dir().ok()?;
48        while !path.join("Cargo.toml").exists() {
49            path = path.parent()?.to_path_buf();
50        }
51        Some(path)
52    }
53
54    // use cargo to detect workspace, falling back to a manual
55    // detection if cargo is not available
56    cargo_detect_workspace().or_else(cwd_detect_workspace)
57}
58
59#[salsa::input(debug)]
60pub struct File {
61    #[returns(ref)]
62    pub path: PathBuf,
63    #[returns(ref)]
64    pub member_file: Option<String>,
65    pub mtime: std::time::SystemTime,
66    pub size: u64,
67}
68
69impl File {
70    /// Builds the `File` input from a file path and an optional member file name.`
71    pub fn build(
72        db: &dyn DwarfDb,
73        path: PathBuf,
74        member_file: Option<String>,
75    ) -> anyhow::Result<File> {
76        // check if we the file needs to be relocated
77        let path = db.remap_path(&path);
78
79        let file = std::fs::File::open(&path).inspect_err(|_| {
80            tracing::warn!("Failed to open file: {}:", path.display());
81        })?;
82        let metadata = file.metadata()?;
83        let mtime = metadata.modified()?;
84        let size = metadata.len();
85
86        Ok(Self::new(db, path, member_file, mtime, size))
87    }
88
89    pub fn name(&self, db: &dyn salsa::Database) -> String {
90        self.path(db).display().to_string()
91    }
92}
93
94#[salsa::input(debug)]
95pub struct Binary {
96    pub file: File,
97}
98
99impl Binary {
100    pub fn name(&self, db: &dyn DwarfDb) -> String {
101        self.file(db).name(db)
102    }
103}
104
105#[salsa::input(debug)]
106#[derive(PartialOrd, Ord)]
107pub struct DebugFile {
108    /// The underlying file/metadata for this debug file
109    pub file: File,
110    /// Whether this debug file is relocatable
111    /// (i..e it is split from the main binary and can be loaded independently)
112    pub relocatable: bool,
113}
114
115impl DebugFile {
116    pub fn name(&self, db: &dyn salsa::Database) -> String {
117        let file = self.file(db);
118        if let Some(member) = file.member_file(db) {
119            format!("{}({})", file.name(db), member)
120        } else {
121            file.name(db)
122        }
123    }
124}
125
126pub struct LoadedFile {
127    filepath: String,
128    file: File,
129    mapped_file: Mmap,
130    pub object: object::File<'static>,
131    pub dwarf: Dwarf,
132}
133
134impl PartialEq for LoadedFile {
135    fn eq(&self, other: &Self) -> bool {
136        self.file == other.file
137    }
138}
139
140impl fmt::Debug for LoadedFile {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        f.debug_struct("LoadedFile")
143            .field("filepath", &self.filepath.to_string())
144            .field("size", &self.mapped_file.len())
145            .field("has_debug_sections", &self.object.has_debug_symbols())
146            .finish()
147    }
148}
149
150#[salsa::tracked(returns(ref))]
151pub fn load<'db>(db: &'db dyn DwarfDb, file: File) -> Result<LoadedFile, Error> {
152    let path = file.path(db);
153    let path = db.remap_path(path);
154    let member = file.member_file(db);
155
156    let file_handle = std::fs::File::open(path)
157        .inspect_err(|_| tracing::warn!("Failed to open file: {}", file.name(db)))?;
158    let mmap = unsafe { memmap2::Mmap::map(&file_handle) }?;
159    let mmap_static_ref = unsafe {
160        // SAFETY: we hold onto the Mmap until the end of the program
161        // and we ensure it lives long enough
162        std::mem::transmute::<&[u8], &'static [u8]>(&*mmap)
163    };
164    let object = if let Some(member) = &member {
165        // we need to extract the object file from the archive
166        let archive = ArchiveFile::parse(mmap_static_ref)?;
167        if let Some(file) = archive.members().find_map(|file| {
168            let Ok(file) = file else {
169                return None;
170            };
171            if file.name() == member.as_bytes() {
172                Some(file)
173            } else {
174                None
175            }
176        }) {
177            // parse the object file from the slice of data
178            // in the archive
179            object::File::parse(file.data(mmap_static_ref)?)?
180        } else {
181            return Err(Error::MemberFileNotFound(format!(
182                "object file {member} not found in archive {}",
183                file.name(db)
184            )));
185        }
186    } else {
187        object::read::File::parse(mmap_static_ref)?
188    };
189    let dwarf = self::loader::load(&object)?;
190
191    Ok(LoadedFile {
192        filepath: if let Some(member) = member {
193            format!("{}({member}", file.name(db))
194        } else {
195            file.name(db)
196        },
197        file,
198        mapped_file: mmap,
199        object,
200        dwarf,
201    })
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, salsa::Update)]
205pub struct SourceFile {
206    pub path: PathBuf,
207}
208
209impl SourceFile {
210    pub fn new(path: PathBuf) -> Self {
211        Self { path }
212    }
213
214    pub fn path_str(&self) -> std::borrow::Cow<'_, str> {
215        self.path.to_string_lossy()
216    }
217
218    pub fn is_external(&self) -> bool {
219        // Check if the source file is external by checking if it is not in the same directory as the binary
220        let current_dir = std::env::current_dir()
221            .unwrap_or_default()
222            .to_string_lossy()
223            .to_string();
224        !self.path.starts_with(&current_dir)
225    }
226}
227
228#[derive(Debug, Clone)]
229pub enum Error {
230    Gimli(gimli::Error),
231    Io(Arc<std::io::Error>),
232    ObjectParseFailure(object::read::Error),
233    MemberFileNotFound(String),
234}
235
236impl fmt::Display for Error {
237    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238        match self {
239            Error::Gimli(error) => write!(f, "Gimli error: {error}"),
240            Error::Io(error) => write!(f, "IO Error: {error}",),
241            Error::MemberFileNotFound(e) => write!(f, "Member file not found: {e}"),
242            Error::ObjectParseFailure(error) => write!(f, "Object parse error: {error}"),
243        }
244    }
245}
246
247impl PartialEq for Error {
248    fn eq(&self, _other: &Self) -> bool {
249        // we'll consider _all_ errors equal for now
250        // since we only really care about if it was an error or not
251        true
252    }
253}
254
255impl std::error::Error for Error {
256    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
257        match self {
258            Error::Gimli(error) => Some(error),
259            Error::Io(error) => Some(error.as_ref()),
260            Error::ObjectParseFailure(error) => Some(error),
261            Error::MemberFileNotFound(_) => None,
262        }
263    }
264}
265
266impl From<std::io::Error> for Error {
267    fn from(err: std::io::Error) -> Self {
268        Error::Io(Arc::new(err))
269    }
270}
271
272impl From<object::read::Error> for Error {
273    fn from(err: object::read::Error) -> Self {
274        Error::ObjectParseFailure(err)
275    }
276}
277
278impl From<gimli::Error> for Error {
279    fn from(err: gimli::Error) -> Self {
280        Error::Gimli(err)
281    }
282}