use {
super::model::SrcLoc,
addr2line::Loader,
object::{Object, ObjectSection},
std::{
collections::BTreeSet,
path::{Path, PathBuf},
},
};
const INSN_SIZE: u64 = 8;
pub struct SourceResolver {
inner: Option<Inner>,
}
struct Inner {
loader: Loader,
text_addr: u64,
text_size: u64,
}
impl SourceResolver {
pub fn from_elf_path(path: &Path) -> Self {
Self {
inner: build(path).ok(),
}
}
pub fn resolve(&self, pc: u64) -> Option<SrcLoc> {
let inner = self.inner.as_ref()?;
let vaddr = inner.text_addr.checked_add(pc.checked_mul(INSN_SIZE)?)?;
let loc = inner.loader.find_location(vaddr).ok().flatten()?;
Some(SrcLoc {
file: PathBuf::from(loc.file?),
line: loc.line?,
})
}
pub fn resolve_frames(&self, pc: u64) -> Vec<SrcLoc> {
let Some(inner) = self.inner.as_ref() else {
return Vec::<SrcLoc>::new();
};
let Some(vaddr) = inner
.text_addr
.checked_add(match pc.checked_mul(INSN_SIZE) {
Some(v) => v,
None => return Vec::<SrcLoc>::new(),
})
else {
return Vec::<SrcLoc>::new();
};
let mut out: Vec<SrcLoc> = Vec::new();
let Ok(mut frames) = inner.loader.find_frames(vaddr) else {
return out;
};
while let Ok(Some(frame)) = frames.next() {
if let Some(loc) = frame.location {
if let (Some(file), Some(line)) = (loc.file, loc.line) {
out.push(SrcLoc {
file: PathBuf::from(file),
line,
});
}
}
}
out
}
pub fn executable_lines(&self) -> Vec<SrcLoc> {
let Some(inner) = self.inner.as_ref() else {
return Vec::new();
};
let Some(text_end) = inner.text_addr.checked_add(inner.text_size) else {
return Vec::new();
};
let Ok(iter) = inner.loader.find_location_range(inner.text_addr, text_end) else {
return Vec::new();
};
let mut lines = BTreeSet::new();
for (_, _, loc) in iter {
if let (Some(file), Some(line)) = (loc.file, loc.line) {
if line != 0 {
lines.insert((PathBuf::from(file), line));
}
}
}
lines
.into_iter()
.map(|(file, line)| SrcLoc { file, line })
.collect()
}
pub fn is_empty(&self) -> bool {
self.inner.is_none()
}
}
#[cfg(target_os = "macos")]
pub const CI_PLATFORM_TOOLS_PREFIX: &str =
"/Users/runner/work/platform-tools/platform-tools/out/rust/library/";
#[cfg(target_os = "linux")]
pub const CI_PLATFORM_TOOLS_PREFIX: &str =
"/home/runner/work/platform-tools/platform-tools/out/rust/library/";
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
pub const CI_PLATFORM_TOOLS_PREFIX: &str = compile_error!("Current platform is not supported");
pub fn discover_platform_tools_stdlib_roots() -> Vec<PathBuf> {
let Some(home) = dirs::home_dir() else {
return Vec::new();
};
let base = home.join(".cache/solana");
let Ok(entries) = std::fs::read_dir(&base) else {
return Vec::new();
};
let mut versions: Vec<(String, PathBuf)> = entries
.flatten()
.filter_map(|e| {
let name = e.file_name().to_str()?.to_owned();
let candidate = e
.path()
.join("platform-tools/rust/lib/rustlib/src/rust/library");
candidate.is_dir().then_some((name, candidate))
})
.collect();
versions.sort_by(|a, b| b.0.cmp(&a.0));
versions.into_iter().map(|(_, p)| p).collect()
}
fn build(path: &Path) -> anyhow::Result<Inner> {
let bytes = std::fs::read(path)?;
let file = object::File::parse(&*bytes)?;
let text = file
.sections()
.find(|s| s.name().ok() == Some(".text"))
.or_else(|| {
file.sections()
.find(|s| s.kind() == object::SectionKind::Text)
})
.ok_or_else(|| anyhow::anyhow!("no .text section"))?;
let text_addr = text.address();
let text_size = text.size();
let loader = Loader::new(path).map_err(|e| anyhow::anyhow!("load DWARF: {e}"))?;
Ok(Inner {
loader,
text_addr,
text_size,
})
}