use crate::args::OutputFormat;
use crate::binary_format::BinaryRef;
use crate::call_tree::filter_allowed_causes;
use crate::call_tree::{
AnalysisSummary, CallTreeNode, CrateCodePoint, build_call_tree_parallel_filtered,
collect_crate_code_points,
};
use crate::config::Config;
use crate::heuristics::{find_entry_points, is_library_panic_symbol};
use crate::project_context::ProjectContext;
use crate::sym::{
CallGraph, DebugInfo, LibraryCallGraph, SymbolIndex, SymbolTable, load_debug_info,
};
use dashmap::DashSet;
use goblin::mach::Mach::Binary;
use indicatif::{ProgressBar, ProgressStyle};
use std::collections::HashSet;
use std::io::{self, IsTerminal};
use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
fn create_spinner(show_progress: bool, message: &str) -> Option<ProgressBar> {
if !show_progress || !io::stderr().is_terminal() {
return None;
}
let spinner = ProgressBar::new_spinner();
spinner.set_style(
ProgressStyle::default_spinner()
.template(" {spinner:.cyan} {msg} [{elapsed}]")
.expect("valid template"),
);
spinner.set_message(message.to_string());
spinner.enable_steady_tick(std::time::Duration::from_millis(100));
Some(spinner)
}
fn finish_spinner(spinner: Option<ProgressBar>, message: &str) {
if let Some(s) = spinner {
s.finish_with_message(message.to_string());
}
}
#[derive(Default)]
pub struct BinaryAnalysisResult {
pub summary: AnalysisSummary,
pub code_points: Vec<CrateCodePoint>,
}
impl BinaryAnalysisResult {
pub fn new() -> Self {
Self::default()
}
fn merge(&mut self, other: BinaryAnalysisResult) {
use std::collections::HashMap;
let mut points_map: HashMap<(String, u32), CrateCodePoint> = self
.code_points
.drain(..)
.map(|cp| ((cp.file.clone(), cp.line), cp))
.collect();
for cp in other.code_points {
let key = (cp.file.clone(), cp.line);
if let Some(existing) = points_map.get_mut(&key) {
existing.causes.extend(cp.causes);
} else {
points_map.insert(key, cp);
}
}
self.code_points = points_map.into_values().collect();
self.code_points
.sort_by(|a, b| a.file.cmp(&b.file).then_with(|| a.line.cmp(&b.line)));
let points: HashSet<_> = self
.code_points
.iter()
.map(|cp| (cp.file.clone(), cp.line))
.collect();
let files: HashSet<_> = self.code_points.iter().map(|cp| cp.file.clone()).collect();
self.summary = AnalysisSummary::from_points(points, files);
}
}
#[derive(Hash, Eq, PartialEq)]
struct PanicCaller {
file: String,
name: String,
line: u32,
column: Option<u32>,
target: String,
}
pub fn analyze_binary_target(
symbols: &SymbolTable,
buffer: &[u8],
binary_path: &Path,
show_timings: bool,
config: &Config,
output: &OutputFormat,
project_context: &ProjectContext,
) -> Result<BinaryAnalysisResult, String> {
let binary_ref = match symbols {
SymbolTable::MachO(Binary(macho)) => BinaryRef::MachO(macho),
SymbolTable::Elf(elf) => BinaryRef::Elf(elf),
_ => return Err("Expected MachO or ELF binary".to_string()),
};
let show_progress = output.show_progress();
let total_start = show_timings.then(Instant::now);
if show_progress {
eprintln!(" Finding entry points...");
}
let step_start = show_timings.then(Instant::now);
let entry_points = find_entry_points(symbols);
if let Some(step_start) = step_start {
eprintln!(" [timing] Find entry points: {:?}", step_start.elapsed());
}
if entry_points.is_empty() {
return Ok(BinaryAnalysisResult::new());
}
if show_progress {
eprintln!(
" Found {} entry points (panic + abort)",
entry_points.len()
);
}
if show_progress {
eprintln!(" Loading debug information...");
}
let step_start = show_timings.then(Instant::now);
let debug_info = load_debug_info(&binary_ref, binary_path, !show_progress);
if let Some(step_start) = step_start {
eprintln!(" [timing] Load debug info: {:?}", step_start.elapsed());
}
let spinner = create_spinner(show_progress, "Scanning for function calls...");
let step_start = show_timings.then(Instant::now);
let symbol_index = SymbolIndex::from_binary(&binary_ref);
let call_graph = match &debug_info {
DebugInfo::Embedded => {
CallGraph::build_with_debug_info(
&binary_ref,
buffer,
&binary_ref,
buffer,
show_timings,
symbol_index.as_ref(),
project_context,
)
.or_else(|e| {
eprintln!("Warning: debug-enriched call graph failed: {e}. Falling back to symbol-only graph.");
CallGraph::build(&binary_ref, buffer, symbol_index.as_ref())
})
.unwrap_or_else(|e| {
eprintln!("Error: call graph build failed: {e}");
CallGraph::empty()
})
}
DebugInfo::DSym(dsym_info) => dsym_info.with_debug_macho(|debug_macho| {
if let Binary(debug_mach) = debug_macho {
let debug_binary_ref = BinaryRef::MachO(debug_mach);
CallGraph::build_with_debug_info(
&binary_ref,
buffer,
&debug_binary_ref,
dsym_info.borrow_debug_buffer(),
show_timings,
symbol_index.as_ref(),
project_context,
)
.or_else(|e| {
eprintln!("Warning: debug-enriched call graph failed: {e}. Falling back to symbol-only graph.");
CallGraph::build(&binary_ref, buffer, symbol_index.as_ref())
})
.unwrap_or_else(|e| {
eprintln!("Error: call graph build failed: {e}");
CallGraph::empty()
})
} else {
CallGraph::build(&binary_ref, buffer, symbol_index.as_ref()).unwrap_or_else(|e| {
eprintln!("Error: call graph build failed: {e}");
CallGraph::empty()
})
}
}),
DebugInfo::DebugMap(_) | DebugInfo::None => {
CallGraph::build(&binary_ref, buffer, symbol_index.as_ref()).unwrap_or_else(|e| {
eprintln!("Error: call graph build failed: {e}");
CallGraph::empty()
})
}
};
finish_spinner(spinner, "Scanning complete");
if let Some(step_start) = step_start {
eprintln!(" [timing] Build call graph: {:?}", step_start.elapsed());
}
let mut final_result = BinaryAnalysisResult::new();
let visited = Arc::new(DashSet::new());
let spinner = create_spinner(show_progress, "Building call trees...");
let step_start = show_timings.then(Instant::now);
for (_mangled, demangled, target_addr) in &entry_points {
if !visited.insert(*target_addr) {
continue;
}
let mut root = CallTreeNode::new_root(demangled.clone());
root.callers =
build_call_tree_parallel_filtered(&call_graph, *target_addr, &visited, project_context);
let (code_points, _summary) = collect_crate_code_points(&root, config, project_context);
let entry_result = BinaryAnalysisResult {
summary: AnalysisSummary::default(),
code_points,
};
final_result.merge(entry_result);
}
let total_nodes = visited.len();
finish_spinner(
spinner,
&format!(
"Built call trees ({} nodes from {} entry points)",
total_nodes,
entry_points.len()
),
);
if let (Some(step_start), Some(total_start)) = (step_start, total_start) {
eprintln!(
" [timing] Build call trees (with pruning): {:?}",
step_start.elapsed()
);
eprintln!(" [timing] TOTAL: {:?}", total_start.elapsed());
}
Ok(final_result)
}
pub fn analyze_archive(
archive: &goblin::archive::Archive,
buffer: &[u8],
binary_path: &Path,
show_timings: bool,
config: &Config,
output: &OutputFormat,
project_context: &ProjectContext,
) -> Result<BinaryAnalysisResult, String> {
let show_progress = output.show_progress();
let total_start = show_timings.then(Instant::now);
if show_progress {
eprintln!(" Building library call graph from relocations...");
}
let step_start = show_timings.then(Instant::now);
let mut merged_graph = LibraryCallGraph::empty();
let lib_name = binary_path
.file_stem()
.and_then(|s| s.to_str())
.and_then(|s| s.strip_prefix("lib"))
.unwrap_or("");
if show_progress {
eprintln!(" DEBUG: lib_name = '{}'", lib_name);
}
let all_members: Vec<String> = archive.members().iter().map(|s| s.to_string()).collect();
let mut member_names = Vec::new();
let mut skipped_count = 0;
let mut sample_kept = Vec::new();
let mut sample_skipped = Vec::new();
for member_name in &all_members {
if !member_name.ends_with(".o") {
continue;
}
if !lib_name.is_empty() {
let normalized_lib_name = lib_name.replace('-', "_");
if !member_name.starts_with(&normalized_lib_name)
&& !member_name.starts_with(&format!("{}-", lib_name))
{
if sample_skipped.len() < 2 {
sample_skipped.push(member_name.to_string());
}
skipped_count += 1;
continue;
} else if sample_kept.len() < 5 {
sample_kept.push(member_name.to_string());
}
}
member_names.push(member_name.clone());
}
if show_progress {
eprintln!(
" Extracting {} .o files from archive...",
member_names.len()
);
}
let extraction_start = show_timings.then(Instant::now);
let member_data_list: Vec<(String, Vec<u8>)> = member_names
.iter()
.filter_map(|member_name| match archive.extract(member_name, buffer) {
Ok(data) => Some((member_name.clone(), data.to_vec())),
Err(e) => {
if show_progress {
eprintln!(" Warning: Failed to extract {}: {}", member_name, e);
}
None
}
})
.collect();
if let Some(extraction_start) = extraction_start {
eprintln!(
" [timing] Extract .o files: {:?}",
extraction_start.elapsed()
);
}
use rayon::prelude::*;
use std::sync::atomic::{AtomicUsize, Ordering};
if show_progress {
eprintln!(
" Processing {} .o files in parallel...",
member_data_list.len()
);
}
let processing_start = show_timings.then(Instant::now);
let processed_count = AtomicUsize::new(0);
let total_count = member_data_list.len();
let graphs: Vec<LibraryCallGraph> = member_data_list
.par_iter()
.filter_map(|(member_name, member_data)| {
let result = match goblin::Object::parse(member_data.as_slice()) {
Ok(goblin::Object::Mach(goblin::mach::Mach::Binary(obj_macho))) => {
let binary_ref = BinaryRef::MachO(&obj_macho);
LibraryCallGraph::build_from_object(
&binary_ref,
member_data.as_slice(),
project_context,
)
.map_err(|e| {
if show_progress {
eprintln!(
" Warning: Failed to build call graph for {}: {}",
member_name, e
);
}
})
.ok()
}
Ok(goblin::Object::Elf(obj_elf)) => {
let binary_ref = BinaryRef::Elf(&obj_elf);
LibraryCallGraph::build_from_object(
&binary_ref,
member_data.as_slice(),
project_context,
)
.map_err(|e| {
if show_progress {
eprintln!(
" Warning: Failed to build call graph for {}: {}",
member_name, e
);
}
})
.ok()
}
_ => {
if show_progress {
eprintln!(" Warning: Failed to parse {} as MachO or ELF", member_name);
}
None
}
};
let count = processed_count.fetch_add(1, Ordering::Relaxed) + 1;
if show_progress && count % 10 == 0 {
eprintln!(" Processing .o files: {}/{}", count, total_count);
}
result
})
.collect();
if let Some(processing_start) = processing_start {
eprintln!(
" [timing] Process .o files (parallel): {:?}",
processing_start.elapsed()
);
}
for graph in graphs {
merged_graph.merge(graph);
}
let processed_count = processed_count.load(Ordering::Relaxed);
if show_progress {
if !sample_kept.is_empty() {
eprintln!(" DEBUG: Sample kept files:");
for file in &sample_kept {
eprintln!(" {}", file);
}
}
if !sample_skipped.is_empty() {
eprintln!(" DEBUG: Sample skipped files:");
for file in &sample_skipped {
eprintln!(" {}", file);
}
}
eprintln!(
" Processed {} .o files ({} stdlib/dependency files skipped)",
processed_count, skipped_count
);
}
if let Some(step_start) = step_start {
eprintln!(
" [timing] Build library call graph: {:?}",
step_start.elapsed()
);
}
if merged_graph.is_empty() {
if show_progress {
println!("\nNo call graph data found in archive");
}
return Ok(BinaryAnalysisResult::new());
}
if show_progress {
eprintln!(" Finding panic callers...");
}
let step_start = show_timings.then(Instant::now);
let mut panic_callers: HashSet<PanicCaller> = HashSet::new();
for target_sym in merged_graph.target_symbols() {
if !is_library_panic_symbol(target_sym) {
continue;
}
for caller_info in merged_graph.get_callers(target_sym) {
let dwarf_file = caller_info
.caller_file
.as_ref()
.filter(|f| project_context.is_crate_source(f));
if let Some(file) = dwarf_file
&& let Some(line) = caller_info.line
{
panic_callers.insert(PanicCaller {
file: file.clone(),
name: caller_info.caller_name.to_string(),
line,
column: caller_info.column,
target: target_sym.to_string(),
});
}
}
}
if let Some(step_start) = step_start {
eprintln!(" [timing] Find panic callers: {:?}", step_start.elapsed());
}
if panic_callers.is_empty() {
if show_progress {
println!("\nNo panics in crate");
}
return Ok(BinaryAnalysisResult::new());
}
let mut sorted_callers: Vec<_> = panic_callers.into_iter().collect();
sorted_callers.sort_by(|a, b| (&a.file, a.line, &a.name).cmp(&(&b.file, b.line, &b.name)));
let mut code_points: Vec<CrateCodePoint> = sorted_callers
.into_iter()
.map(|caller| {
let mut causes = HashSet::new();
if let Some(cause) = crate::heuristics::detect_panic_cause(&caller.target) {
causes.insert(cause);
}
CrateCodePoint {
name: caller.name,
file: caller.file,
line: caller.line,
column: caller.column,
causes,
children: Vec::new(), is_direct_panic: true, called_function: None, }
})
.collect();
for point in &mut code_points {
if point.causes.is_empty() {
point.causes.insert(crate::panic_cause::PanicCause::Unknown);
}
}
filter_allowed_causes(&mut code_points, config, project_context);
let mut seen: std::collections::HashMap<(String, u32), usize> =
std::collections::HashMap::new();
let mut deduped: Vec<CrateCodePoint> = Vec::new();
for point in code_points {
let key = (point.file.clone(), point.line);
if let Some(&idx) = seen.get(&key) {
deduped[idx].causes.extend(point.causes);
} else {
seen.insert(key, deduped.len());
deduped.push(point);
}
}
if let Some(total_start) = total_start {
eprintln!(" [timing] TOTAL: {:?}", total_start.elapsed());
}
let mut points: HashSet<(String, u32)> = HashSet::new();
let mut files_affected: HashSet<String> = HashSet::new();
for point in &deduped {
points.insert((point.file.clone(), point.line));
files_affected.insert(point.file.clone());
}
Ok(BinaryAnalysisResult {
summary: AnalysisSummary::from_points(points, files_affected),
code_points: deduped,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::panic_cause::PanicCause;
#[test]
fn test_binary_analysis_result_empty() {
let result = BinaryAnalysisResult::new();
assert!(result.code_points.is_empty());
assert_eq!(result.summary.panic_points(), 0);
assert_eq!(result.summary.files_affected(), 0);
}
fn make_code_point(file: &str, line: u32, cause: PanicCause) -> CrateCodePoint {
let mut causes = HashSet::new();
causes.insert(cause);
CrateCodePoint {
name: "test_func".to_string(),
file: file.to_string(),
line,
column: None,
causes,
children: Vec::new(),
is_direct_panic: true,
called_function: None,
}
}
#[test]
fn test_binary_analysis_result_merge_disjoint() {
let mut result1 = BinaryAnalysisResult {
summary: AnalysisSummary::default(),
code_points: vec![make_code_point("src/a.rs", 10, PanicCause::Unwrap)],
};
let result2 = BinaryAnalysisResult {
summary: AnalysisSummary::default(),
code_points: vec![make_code_point("src/b.rs", 20, PanicCause::Unwrap)],
};
result1.merge(result2);
assert_eq!(result1.code_points.len(), 2);
assert_eq!(result1.summary.panic_points(), 2);
assert_eq!(result1.summary.files_affected(), 2);
}
#[test]
fn test_binary_analysis_result_merge_same_location() {
let mut result1 = BinaryAnalysisResult {
summary: AnalysisSummary::default(),
code_points: vec![make_code_point("src/main.rs", 10, PanicCause::Unwrap)],
};
let result2 = BinaryAnalysisResult {
summary: AnalysisSummary::default(),
code_points: vec![make_code_point("src/main.rs", 10, PanicCause::Expect)],
};
result1.merge(result2);
assert_eq!(result1.code_points.len(), 1);
assert_eq!(result1.code_points[0].causes.len(), 2);
assert!(result1.code_points[0].causes.contains(&PanicCause::Unwrap));
assert!(result1.code_points[0].causes.contains(&PanicCause::Expect));
}
#[test]
fn test_binary_analysis_result_merge_sorted() {
let mut result1 = BinaryAnalysisResult {
summary: AnalysisSummary::default(),
code_points: vec![make_code_point("src/z.rs", 100, PanicCause::Unwrap)],
};
let result2 = BinaryAnalysisResult {
summary: AnalysisSummary::default(),
code_points: vec![
make_code_point("src/a.rs", 10, PanicCause::Unwrap),
make_code_point("src/a.rs", 5, PanicCause::ExplicitPanic),
],
};
result1.merge(result2);
assert_eq!(result1.code_points.len(), 3);
assert_eq!(result1.code_points[0].file, "src/a.rs");
assert_eq!(result1.code_points[0].line, 5);
assert_eq!(result1.code_points[1].file, "src/a.rs");
assert_eq!(result1.code_points[1].line, 10);
assert_eq!(result1.code_points[2].file, "src/z.rs");
assert_eq!(result1.code_points[2].line, 100);
}
#[test]
fn test_binary_analysis_result_merge_empty() {
let mut result1 = BinaryAnalysisResult {
summary: AnalysisSummary::default(),
code_points: vec![make_code_point("src/main.rs", 10, PanicCause::Unwrap)],
};
let result2 = BinaryAnalysisResult::new();
result1.merge(result2);
assert_eq!(result1.code_points.len(), 1);
}
}