use crate::binary_format::BinaryRef;
use goblin::Object;
use goblin::mach::symbols::N_OSO;
use goblin::mach::{Mach, MachO};
use ouroboros::self_referencing;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
#[self_referencing]
pub struct DSymInfo {
pub debug_buffer: Vec<u8>,
#[borrows(debug_buffer)]
#[covariant]
pub debug_macho: Mach<'this>,
}
#[derive(Debug)]
pub struct ObjectFileInfo {
pub path: PathBuf,
pub buffer: Vec<u8>,
pub addr_map: HashMap<u64, u64>,
}
pub struct DebugMapInfo {
pub object_files: Vec<ObjectFileInfo>,
}
pub enum DebugInfo {
Embedded,
DSym(Box<DSymInfo>),
DebugMap(Box<DebugMapInfo>),
None,
}
fn has_dwarf_sections(macho: &MachO) -> bool {
for segment in macho.segments.iter() {
if let Ok(name) = segment.name()
&& name == "__DWARF"
{
return true;
}
if let Ok(sections) = segment.sections() {
for (section, _) in sections {
if let Ok(name) = section.name()
&& name.starts_with("__debug_")
{
return true;
}
}
}
}
false
}
fn get_oso_paths(macho: &MachO) -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Some(symbols) = &macho.symbols {
for (name, nlist) in symbols.iter().flatten() {
if nlist.n_type == N_OSO && !name.is_empty() {
paths.push(PathBuf::from(name));
}
}
}
paths.sort();
paths.dedup();
paths
}
fn build_addr_translation_map(binary_macho: &MachO, obj_macho: &MachO) -> HashMap<u64, u64> {
let mut addr_map = HashMap::new();
let Some(binary_symbols) = &binary_macho.symbols else {
return addr_map;
};
let Some(obj_symbols) = &obj_macho.symbols else {
return addr_map;
};
let mut binary_sym_addrs: HashMap<String, u64> = HashMap::new();
for (name, nlist) in binary_symbols.iter().flatten() {
if nlist.n_value > 0 && !name.is_empty() {
binary_sym_addrs.insert(name.to_string(), nlist.n_value);
}
}
for (name, nlist) in obj_symbols.iter().flatten() {
if nlist.n_value > 0
&& !name.is_empty()
&& let Some(&binary_addr) = binary_sym_addrs.get(name)
{
addr_map.insert(nlist.n_value, binary_addr);
}
}
addr_map
}
fn is_dsym_stale(binary_path: &Path, dsym_path: &Path) -> bool {
let binary_modified = match fs::metadata(binary_path).and_then(|m| m.modified()) {
Ok(t) => t,
Err(_) => return false, };
let dsym_modified = match fs::metadata(dsym_path).and_then(|m| m.modified()) {
Ok(t) => t,
Err(_) => return true, };
binary_modified > dsym_modified
}
pub fn load_debug_info(binary: &BinaryRef, binary_path: &Path, quiet: bool) -> DebugInfo {
if binary.is_elf() {
if binary.has_dwarf() {
if !quiet {
println!(" Using embedded DWARF debugging info");
}
return DebugInfo::Embedded;
}
if !quiet {
println!(" No debug info found in ELF binary");
}
return DebugInfo::None;
}
let BinaryRef::MachO(macho) = binary else {
unreachable!("ELF handled above");
};
let file_name = binary_path.file_name().unwrap().to_str().unwrap();
let file_stem = binary_path.file_stem().unwrap().to_str().unwrap();
let dsym_base = binary_path.parent().unwrap_or(Path::new("."));
let dsym_paths = [
dsym_base
.join(format!("{}.dSYM", file_stem))
.join("Contents/Resources/DWARF")
.join(file_name),
dsym_base
.join(format!("{}.dSYM", file_stem))
.join("Contents/Resources/DWARF")
.join(file_stem),
binary_path
.with_extension("dSYM")
.join("Contents/Resources/DWARF")
.join(file_name),
];
for dsym_path in &dsym_paths {
if dsym_path.exists() {
let dsym_stale = is_dsym_stale(binary_path, dsym_path);
if dsym_stale {
if !quiet {
println!(" dSYM is stale, will regenerate");
}
} else {
if !quiet {
println!(" Using .dSYM bundle for debug info");
}
let debug_buffer = fs::read(dsym_path).unwrap();
let dsym_info = DSymInfoBuilder {
debug_buffer,
debug_macho_builder: |buf: &Vec<u8>| Mach::parse(buf).unwrap(),
}
.build();
return DebugInfo::DSym(Box::new(dsym_info));
}
}
}
if binary.has_dwarf() {
if !quiet {
println!(" Using embedded DWARF debugging info");
}
return DebugInfo::Embedded;
}
if let Some(dsym_info) = auto_generate_dsym(binary_path, quiet) {
return DebugInfo::DSym(Box::new(dsym_info));
}
if let Some(debug_map) = load_debug_map(macho, quiet) {
return DebugInfo::DebugMap(Box::new(debug_map));
}
if !quiet {
println!(" No debug info found (no dSYM, embedded DWARF, or debug map)");
println!(
"Tip: Install dsymutil or run 'dsymutil {}' to generate debug symbols",
binary_path.display()
);
}
DebugInfo::None
}
fn auto_generate_dsym(binary_path: &Path, quiet: bool) -> Option<DSymInfo> {
use std::process::Command;
if !binary_path.exists() {
return None;
}
let dsym_path = binary_path.with_extension("dSYM");
let status = Command::new("dsymutil")
.arg(binary_path)
.arg("-o")
.arg(&dsym_path)
.stderr(std::process::Stdio::null())
.status()
.ok()?;
if !status.success() {
return None;
}
let file_name = binary_path.file_name()?.to_str()?;
let file_stem = binary_path.file_stem()?.to_str()?;
let dwarf_paths = [
dsym_path.join("Contents/Resources/DWARF").join(file_name),
dsym_path.join("Contents/Resources/DWARF").join(file_stem),
];
for dwarf_path in &dwarf_paths {
if dwarf_path.exists() {
if !quiet {
println!(" Generated .dSYM bundle for debug info");
}
let debug_buffer = fs::read(dwarf_path).ok()?;
let dsym_info = DSymInfoBuilder {
debug_buffer,
debug_macho_builder: |buf: &Vec<u8>| Mach::parse(buf).unwrap(),
}
.build();
return Some(dsym_info);
}
}
None
}
fn load_debug_map(macho: &MachO, quiet: bool) -> Option<DebugMapInfo> {
let oso_paths = get_oso_paths(macho);
if oso_paths.is_empty() {
return None;
}
let mut object_files = Vec::new();
let mut loaded_count = 0;
for path in oso_paths {
if !path.exists() {
continue;
}
let Ok(buffer) = fs::read(&path) else {
continue;
};
let Ok(Object::Mach(Mach::Binary(obj_macho))) = Object::parse(&buffer) else {
continue;
};
if !has_dwarf_sections(&obj_macho) {
continue;
}
let addr_map = build_addr_translation_map(macho, &obj_macho);
object_files.push(ObjectFileInfo {
path,
buffer,
addr_map,
});
loaded_count += 1;
}
if object_files.is_empty() {
return None;
}
if !quiet {
println!(
"Using debug map: loaded {} object files with DWARF",
loaded_count
);
}
Some(DebugMapInfo { object_files })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_dsym_stale_binary_newer() {
use std::fs;
use std::thread;
use std::time::Duration;
let temp_dir = std::env::temp_dir().join("jonesy_test_dsym_stale");
let _ = fs::create_dir_all(&temp_dir);
let binary_path = temp_dir.join("test_binary");
let dsym_path = temp_dir.join("test_binary.dSYM");
fs::write(&dsym_path, "fake dsym").unwrap();
thread::sleep(Duration::from_millis(50));
fs::write(&binary_path, "fake binary").unwrap();
assert!(
is_dsym_stale(&binary_path, &dsym_path),
"dSYM should be stale when binary is newer"
);
let _ = fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_is_dsym_stale_dsym_newer() {
use std::fs;
use std::thread;
use std::time::Duration;
let temp_dir = std::env::temp_dir().join("jonesy_test_dsym_fresh");
let _ = fs::create_dir_all(&temp_dir);
let binary_path = temp_dir.join("test_binary");
let dsym_path = temp_dir.join("test_binary.dSYM");
fs::write(&binary_path, "fake binary").unwrap();
thread::sleep(Duration::from_millis(50));
fs::write(&dsym_path, "fake dsym").unwrap();
assert!(
!is_dsym_stale(&binary_path, &dsym_path),
"dSYM should not be stale when dSYM is newer"
);
let _ = fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_is_dsym_stale_binary_not_found() {
use std::fs;
let temp_dir = std::env::temp_dir().join("jonesy_test_dsym_no_binary");
let _ = fs::create_dir_all(&temp_dir);
let binary_path = temp_dir.join("nonexistent_binary");
let dsym_path = temp_dir.join("test.dSYM");
fs::write(&dsym_path, "fake dsym").unwrap();
assert!(
!is_dsym_stale(&binary_path, &dsym_path),
"Should return false when binary doesn't exist"
);
let _ = fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_is_dsym_stale_dsym_not_found() {
use std::fs;
let temp_dir = std::env::temp_dir().join("jonesy_test_dsym_no_dsym");
let _ = fs::create_dir_all(&temp_dir);
let binary_path = temp_dir.join("test_binary");
let dsym_path = temp_dir.join("nonexistent.dSYM");
fs::write(&binary_path, "fake binary").unwrap();
assert!(
is_dsym_stale(&binary_path, &dsym_path),
"Should return true when dSYM doesn't exist"
);
let _ = fs::remove_dir_all(&temp_dir);
}
#[cfg(target_os = "macos")]
fn panic_binary_path() -> PathBuf {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let workspace_root = manifest_dir.parent().unwrap();
workspace_root.join("target/debug/panic")
}
#[cfg(target_os = "macos")]
fn parse_macho(path: &Path) -> (Vec<u8>, goblin::mach::MachO<'static>) {
let buffer = fs::read(path).expect("binary should exist — run `cargo build` first");
let buf: &'static [u8] = Vec::leak(buffer.clone());
match Object::parse(buf).expect("should parse as Mach-O") {
Object::Mach(Mach::Binary(macho)) => (buffer, macho),
_ => panic!("expected a single Mach-O binary"),
}
}
#[cfg(target_os = "macos")]
#[test]
fn test_has_dwarf_sections_on_real_binary() {
let path = panic_binary_path();
if !path.exists() {
return; }
let (_buf, macho) = parse_macho(&path);
let _has_dwarf = has_dwarf_sections(&macho);
}
#[cfg(target_os = "macos")]
#[test]
fn test_get_oso_paths_on_real_binary() {
let path = panic_binary_path();
if !path.exists() {
return;
}
let (_buf, macho) = parse_macho(&path);
let paths = get_oso_paths(&macho);
let _ = paths; }
#[cfg(target_os = "macos")]
#[test]
fn test_build_addr_translation_map_on_real_binary() {
let path = panic_binary_path();
if !path.exists() {
return;
}
let (_buf, macho) = parse_macho(&path);
let addr_map = build_addr_translation_map(&macho, &macho);
assert!(
!addr_map.is_empty(),
"Self-translation map should have entries for matching symbols"
);
}
#[cfg(target_os = "macos")]
#[test]
fn test_load_debug_info_on_real_binary() {
let path = panic_binary_path();
if !path.exists() {
return;
}
let (_buf, macho) = parse_macho(&path);
let binary_ref = BinaryRef::MachO(&macho);
let info = load_debug_info(&binary_ref, &path, true);
assert!(
!matches!(info, DebugInfo::None),
"Debug build should have debug info"
);
}
#[cfg(target_os = "macos")]
#[test]
fn test_load_debug_info_nonexistent_binary() {
let path = panic_binary_path();
if !path.exists() {
return;
}
let (_buf, macho) = parse_macho(&path);
let binary_ref = BinaryRef::MachO(&macho);
let fake_path = Path::new("/nonexistent/binary");
let _info = load_debug_info(&binary_ref, fake_path, true);
}
}