use anyhow::{Context, Result};
use object::{Object, ObjectSection};
use std::fs::File;
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceLocation {
pub file: String,
pub line: u32,
pub column: Option<u32>,
pub function: Option<String>,
}
pub struct DwarfContext {
context: addr2line::Context<gimli::EndianRcSlice<gimli::RunTimeEndian>>,
}
impl std::fmt::Debug for DwarfContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DwarfContext").field("context", &"<addr2line context>").finish()
}
}
impl DwarfContext {
#[allow(unsafe_code)]
pub fn load(binary_path: &Path) -> Result<Self> {
if !binary_path.exists() {
anyhow::bail!("Binary does not exist: {}", binary_path.display());
}
let file = File::open(binary_path)
.with_context(|| format!("Failed to open binary: {}", binary_path.display()))?;
let mmap = unsafe { memmap2::Mmap::map(&file) }.context("Failed to memory-map binary")?;
let object = object::File::parse(&*mmap).context("Failed to parse ELF binary")?;
let endian = if object.is_little_endian() {
gimli::RunTimeEndian::Little
} else {
gimli::RunTimeEndian::Big
};
let load_section =
|id: gimli::SectionId| -> Result<gimli::EndianRcSlice<gimli::RunTimeEndian>> {
let data = object
.section_by_name(id.name())
.and_then(|section| section.uncompressed_data().ok())
.unwrap_or(std::borrow::Cow::Borrowed(&[]));
let bytes: std::rc::Rc<[u8]> = std::rc::Rc::from(data.into_owned());
Ok(gimli::EndianRcSlice::new(bytes, endian))
};
let dwarf = gimli::Dwarf::load(&load_section)
.context("Failed to load DWARF sections - binary may not have debug symbols. Compile with -g flag.")?;
let context =
addr2line::Context::from_dwarf(dwarf).context("Failed to create DWARF context")?;
Ok(Self { context })
}
pub fn lookup(&self, ip: u64) -> Result<Option<SourceLocation>> {
for offset in [0, 1, 2, 4, 8, 16] {
let adjusted_ip = ip.saturating_sub(offset);
let location = match self.context.find_location(adjusted_ip) {
Ok(Some(loc)) => loc,
Ok(None) => continue,
Err(_) => continue,
};
let file = match location.file {
Some(f) => {
if f.contains("/rustc/") || f.contains("library/") {
continue;
}
f
}
None => continue,
};
let line = location.line.unwrap_or(0);
if line == 0 {
continue; }
let column = location.column;
let mut function_name = None;
let frames_result = self.context.find_frames(adjusted_ip);
if let Ok(mut frames_iter) = frames_result.skip_all_loads() {
if let Ok(Some(frame)) = frames_iter.next() {
if let Some(func) = frame.function {
let raw_name = func.raw_name().ok();
if let Some(name) = raw_name {
function_name = Some(name.to_string());
}
}
}
}
return Ok(Some(SourceLocation {
file: file.to_string(),
line,
column,
function: function_name,
}));
}
Ok(None)
}
}
static_assertions::assert_impl_all!(SourceLocation: Send, Sync);
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::process::Command;
use tempfile::TempDir;
fn compile_test_binary() -> (TempDir, std::path::PathBuf) {
let temp_dir = TempDir::new().expect("test");
let src_file = temp_dir.path().join("test.rs");
let bin_file = temp_dir.path().join("test_bin");
fs::write(&src_file, "fn main() { println!(\"test\"); }").expect("test");
Command::new("rustc")
.arg(&src_file)
.arg("-o")
.arg(&bin_file)
.arg("-g")
.status()
.expect("test");
(temp_dir, bin_file)
}
#[test]
fn test_dwarf_context_loads() {
let (_temp_dir, bin_file) = compile_test_binary();
let result = DwarfContext::load(&bin_file);
assert!(result.is_ok(), "Should load DWARF context: {:?}", result.err());
}
#[test]
fn test_dwarf_lookup_returns_option() {
let (_temp_dir, bin_file) = compile_test_binary();
let ctx = DwarfContext::load(&bin_file).expect("test");
let result = ctx.lookup(0x1000);
assert!(result.is_ok(), "Lookup should not crash: {:?}", result.err());
}
#[test]
fn test_dwarf_load_nonexistent_file() {
let result = DwarfContext::load(std::path::Path::new("/nonexistent/binary"));
assert!(result.is_err(), "Should fail for nonexistent file");
let err = result.unwrap_err().to_string();
assert!(err.contains("does not exist"), "Error should mention file doesn't exist: {}", err);
}
#[test]
fn test_dwarf_load_invalid_binary() {
let temp_dir = TempDir::new().expect("test");
let invalid_file = temp_dir.path().join("invalid.bin");
fs::write(&invalid_file, b"not a valid ELF file").expect("test");
let result = DwarfContext::load(&invalid_file);
assert!(result.is_err(), "Should fail for invalid ELF");
}
#[test]
fn test_dwarf_load_no_debug_symbols() {
let temp_dir = TempDir::new().expect("test");
let src_file = temp_dir.path().join("test.rs");
let bin_file = temp_dir.path().join("test_bin_stripped");
fs::write(&src_file, "fn main() {}").expect("test");
Command::new("rustc")
.arg(&src_file)
.arg("-o")
.arg(&bin_file)
.arg("-C")
.arg("strip=symbols")
.status()
.expect("test");
let result = DwarfContext::load(&bin_file);
let _ = result;
}
#[test]
fn test_dwarf_lookup_with_valid_address() {
let (_temp_dir, bin_file) = compile_test_binary();
let ctx = DwarfContext::load(&bin_file).expect("test");
for offset in [0, 1, 2, 4, 8, 16] {
let _ = ctx.lookup(0x1000 + offset);
}
}
#[test]
fn test_dwarf_lookup_zero_address() {
let (_temp_dir, bin_file) = compile_test_binary();
let ctx = DwarfContext::load(&bin_file).expect("test");
let result = ctx.lookup(0);
assert!(result.is_ok(), "Lookup of zero address should not crash");
}
#[test]
fn test_dwarf_lookup_high_address() {
let (_temp_dir, bin_file) = compile_test_binary();
let ctx = DwarfContext::load(&bin_file).expect("test");
let result = ctx.lookup(0xFFFFFFFFFFFF);
assert!(result.is_ok(), "Lookup of high address should not crash");
}
#[test]
fn test_source_location_clone() {
let loc1 = SourceLocation {
file: "test.rs".to_string(),
line: 42,
column: Some(10),
function: Some("main".to_string()),
};
let loc2 = loc1.clone();
assert_eq!(loc1, loc2, "Cloned SourceLocation should be equal");
}
#[test]
fn test_source_location_debug() {
let loc = SourceLocation {
file: "test.rs".to_string(),
line: 42,
column: Some(10),
function: Some("main".to_string()),
};
let debug_str = format!("{:?}", loc);
assert!(debug_str.contains("test.rs"), "Debug should contain file: {}", debug_str);
assert!(debug_str.contains("42"), "Debug should contain line: {}", debug_str);
}
#[test]
fn test_source_location_no_column() {
let loc =
SourceLocation { file: "lib.rs".to_string(), line: 100, column: None, function: None };
assert_eq!(loc.file, "lib.rs");
assert_eq!(loc.line, 100);
assert_eq!(loc.column, None);
assert_eq!(loc.function, None);
}
#[test]
fn test_source_location_equality() {
let loc1 = SourceLocation {
file: "main.rs".to_string(),
line: 1,
column: Some(5),
function: Some("foo".to_string()),
};
let loc2 = SourceLocation {
file: "main.rs".to_string(),
line: 1,
column: Some(5),
function: Some("foo".to_string()),
};
let loc3 = SourceLocation {
file: "main.rs".to_string(),
line: 2,
column: Some(5),
function: Some("foo".to_string()),
};
assert_eq!(loc1, loc2);
assert_ne!(loc1, loc3);
}
#[test]
fn test_dwarf_context_debug() {
let (_temp_dir, bin_file) = compile_test_binary();
let ctx = DwarfContext::load(&bin_file).expect("test");
let debug_str = format!("{:?}", ctx);
assert!(debug_str.contains("DwarfContext"));
assert!(debug_str.contains("addr2line context"));
}
#[test]
fn test_dwarf_lookup_multiple_addresses() {
let (_temp_dir, bin_file) = compile_test_binary();
let ctx = DwarfContext::load(&bin_file).expect("test");
for addr in [0x1000, 0x2000, 0x3000, 0x4000, 0x5000] {
let result = ctx.lookup(addr);
assert!(result.is_ok(), "Lookup should not crash for addr {:#x}", addr);
}
}
#[test]
fn test_dwarf_lookup_negative_offset() {
let (_temp_dir, bin_file) = compile_test_binary();
let ctx = DwarfContext::load(&bin_file).expect("test");
let result = ctx.lookup(0);
assert!(result.is_ok());
let result = ctx.lookup(1);
assert!(result.is_ok());
}
#[test]
fn test_dwarf_load_real_binary() {
let exe_path = std::env::current_exe().expect("Failed to get current exe");
let result = DwarfContext::load(&exe_path);
match result {
Ok(ctx) => {
let lookup_result = ctx.lookup(0x1000);
assert!(lookup_result.is_ok());
}
Err(e) => {
let err_msg = e.to_string();
assert!(!err_msg.is_empty(), "Error message should not be empty");
}
}
}
#[test]
fn test_source_location_with_column() {
let loc = SourceLocation {
file: "test.rs".to_string(),
line: 10,
column: Some(20),
function: Some("test_fn".to_string()),
};
assert_eq!(loc.column, Some(20));
assert_eq!(loc.function, Some("test_fn".to_string()));
}
#[test]
fn test_dwarf_lookup_extensive_coverage() {
let temp_dir = TempDir::new().expect("test");
let src_file = temp_dir.path().join("complex.rs");
let bin_file = temp_dir.path().join("complex_bin");
fs::write(
&src_file,
r#"
fn helper(x: i32) -> i32 {
x + 1
}
fn main() {
let a = helper(5);
let b = helper(10);
println!("Result: {} {}", a, b);
}
"#,
)
.expect("test");
Command::new("rustc")
.arg(&src_file)
.arg("-o")
.arg(&bin_file)
.arg("-g")
.status()
.expect("test");
let ctx = DwarfContext::load(&bin_file).expect("test");
for addr in 0x1000..0x1100 {
let _ = ctx.lookup(addr);
}
let _ = ctx.lookup(0x7FFF_FFFF_FFFF_FFF0);
let _ = ctx.lookup(0x7FFF_FFFF_FFFF_FFFF);
let _ = ctx.lookup(0);
let _ = ctx.lookup(1);
let _ = ctx.lookup(2);
let _ = ctx.lookup(4);
let _ = ctx.lookup(8);
let _ = ctx.lookup(16);
}
#[test]
fn test_source_location_all_variants() {
let loc1 = SourceLocation {
file: "a.rs".to_string(),
line: 1,
column: Some(1),
function: Some("f".to_string()),
};
let loc2 =
SourceLocation { file: "b.rs".to_string(), line: 2, column: Some(2), function: None };
let loc3 = SourceLocation {
file: "c.rs".to_string(),
line: 3,
column: None,
function: Some("g".to_string()),
};
let loc4 =
SourceLocation { file: "d.rs".to_string(), line: 4, column: None, function: None };
assert_eq!(loc1.file, "a.rs");
assert_eq!(loc2.file, "b.rs");
assert_eq!(loc3.file, "c.rs");
assert_eq!(loc4.file, "d.rs");
assert_ne!(loc1, loc2);
assert_ne!(loc2, loc3);
assert_ne!(loc3, loc4);
}
}