threatflux_binary_analysis/analysis/
call_graph.rs

1//! Call graph analysis for binary programs
2//!
3//! This module provides functionality to analyze function call relationships in binary programs,
4//! including call graph construction, cycle detection, and visualization export.
5
6use crate::{
7    disasm::Disassembler,
8    types::{
9        CallContext, CallGraph, CallGraphConfig, CallGraphEdge, CallGraphNode, CallGraphStatistics,
10        CallSite, CallType, Function, Instruction, NodeType,
11    },
12    BinaryError, BinaryFile, Result,
13};
14use std::collections::{HashMap, HashSet, VecDeque};
15
16/// Call graph analyzer
17#[derive(Clone)]
18pub struct CallGraphAnalyzer {
19    /// Analysis configuration
20    config: CallGraphConfig,
21}
22
23impl CallGraphAnalyzer {
24    /// Create a new call graph analyzer
25    pub fn new(config: CallGraphConfig) -> Self {
26        Self { config }
27    }
28
29    /// Create analyzer with default configuration
30    pub fn new_default() -> Self {
31        Self {
32            config: CallGraphConfig::default(),
33        }
34    }
35
36    /// Analyze binary to construct call graph
37    pub fn analyze_binary(&self, binary: &BinaryFile) -> Result<CallGraph> {
38        // Extract functions from binary
39        let functions = self.extract_functions(binary)?;
40
41        // Build call graph nodes
42        let mut nodes = Vec::new();
43        let mut address_to_node: HashMap<u64, usize> = HashMap::new();
44
45        for (i, function) in functions.iter().enumerate() {
46            let node = CallGraphNode {
47                function_address: function.start_address,
48                function_name: function.name.clone(),
49                node_type: self.classify_node_type(function, binary),
50                complexity: 0,       // Will be calculated later
51                in_degree: 0,        // Will be calculated later
52                out_degree: 0,       // Will be calculated later
53                is_recursive: false, // Will be detected later
54                call_depth: None,    // Will be calculated later
55            };
56            nodes.push(node);
57            address_to_node.insert(function.start_address, i);
58        }
59
60        // Extract function calls and build edges
61        let edges = self.extract_function_calls(binary, &functions, &address_to_node)?;
62
63        // Update node degrees
64        self.update_node_degrees(&mut nodes, &edges);
65
66        // Detect recursion
67        self.detect_recursion(&mut nodes, &edges);
68
69        // Find entry points
70        let entry_points = self.find_entry_points(binary, &functions);
71
72        // Calculate call depths
73        let mut call_graph = CallGraph {
74            nodes,
75            edges,
76            entry_points: entry_points.clone(),
77            unreachable_functions: Vec::new(),
78            statistics: CallGraphStatistics::default(),
79        };
80
81        self.compute_call_depths(&mut call_graph)?;
82
83        // Find unreachable functions
84        call_graph.unreachable_functions = self.find_unreachable_functions(&call_graph);
85
86        // Compute statistics
87        call_graph.statistics = self.compute_statistics(&call_graph);
88
89        Ok(call_graph)
90    }
91
92    /// Extract functions from binary symbols and analysis
93    fn extract_functions(&self, binary: &BinaryFile) -> Result<Vec<Function>> {
94        let mut functions = Vec::new();
95        let mut seen_addresses = HashSet::new();
96
97        // Extract from symbols
98        for symbol in binary.symbols() {
99            if matches!(symbol.symbol_type, crate::types::SymbolType::Function)
100                && symbol.size > 0
101                && !seen_addresses.contains(&symbol.address)
102            {
103                let function = Function {
104                    name: symbol.name.clone(),
105                    start_address: symbol.address,
106                    end_address: symbol.address + symbol.size,
107                    size: symbol.size,
108                    function_type: crate::types::FunctionType::Normal,
109                    calling_convention: None,
110                    parameters: Vec::new(),
111                    return_type: None,
112                };
113                functions.push(function);
114                seen_addresses.insert(symbol.address);
115            }
116        }
117
118        // Add entry point if not already present
119        if let Some(entry_point) = binary.entry_point() {
120            if !seen_addresses.contains(&entry_point) {
121                let function = Function {
122                    name: "_start".to_string(),
123                    start_address: entry_point,
124                    end_address: entry_point + 1000, // Estimate
125                    size: 1000,
126                    function_type: crate::types::FunctionType::Entrypoint,
127                    calling_convention: None,
128                    parameters: Vec::new(),
129                    return_type: None,
130                };
131                functions.push(function);
132            }
133        }
134
135        Ok(functions)
136    }
137
138    /// Classify node type based on function characteristics
139    fn classify_node_type(&self, function: &Function, binary: &BinaryFile) -> NodeType {
140        // Check if it's an entry point
141        if matches!(
142            function.function_type,
143            crate::types::FunctionType::Entrypoint
144        ) || matches!(function.function_type, crate::types::FunctionType::Main)
145            || function.name == "_start"
146            || function.name == "main"
147            || function.name == "DllMain"
148        {
149            return NodeType::EntryPoint;
150        }
151
152        // Check if it's an imported function
153        for import in binary.imports() {
154            if let Some(addr) = import.address {
155                if addr == function.start_address {
156                    return NodeType::External;
157                }
158            }
159        }
160
161        // Check if it's a library function (heuristic based on name)
162        if self.is_library_function(&function.name) {
163            return NodeType::Library;
164        }
165
166        // Default to internal function
167        NodeType::Internal
168    }
169
170    /// Check if a function name indicates a library function
171    fn is_library_function(&self, name: &str) -> bool {
172        // Common library function prefixes (be more specific to avoid false positives)
173        const LIBRARY_PREFIXES: &[&str] =
174            &["libc_", "libm_", "__", "_GLOBAL_", "std::", "_ZN", "_Z"];
175
176        // Common library function exact names or patterns
177        const LIBRARY_NAMES: &[&str] = &[
178            "printf", "scanf", "malloc", "free", "strlen", "strcpy", "strcat", "memcpy", "memset",
179            "fopen", "fclose", "fread", "fwrite", "msvcrt", "kernel32", "ntdll",
180        ];
181
182        // Check prefixes
183        for prefix in LIBRARY_PREFIXES {
184            if name.starts_with(prefix) {
185                return true;
186            }
187        }
188
189        // Check exact names or if they contain library module names
190        for lib_name in LIBRARY_NAMES {
191            if name == *lib_name || name.contains(lib_name) {
192                return true;
193            }
194        }
195
196        false
197    }
198
199    /// Extract function calls from binary analysis
200    fn extract_function_calls(
201        &self,
202        binary: &BinaryFile,
203        functions: &[Function],
204        address_to_node: &HashMap<u64, usize>,
205    ) -> Result<Vec<CallGraphEdge>> {
206        let mut edges = Vec::new();
207        let disassembler = Disassembler::new(binary.architecture())?;
208
209        for function in functions {
210            // Get instructions for this function
211            if let Ok(instructions) =
212                self.get_function_instructions(binary, function, &disassembler)
213            {
214                // Analyze instructions for calls
215                for instruction in &instructions {
216                    if let Some(edge) = self.analyze_call_instruction(
217                        instruction,
218                        function.start_address,
219                        address_to_node,
220                    ) {
221                        edges.push(edge);
222                    }
223                }
224            }
225        }
226
227        // Detect tail calls if enabled
228        if self.config.detect_tail_calls {
229            let tail_call_edges = self.detect_tail_calls(binary, functions, address_to_node)?;
230            edges.extend(tail_call_edges);
231        }
232
233        Ok(edges)
234    }
235
236    /// Get instructions for a function
237    fn get_function_instructions(
238        &self,
239        binary: &BinaryFile,
240        function: &Function,
241        disassembler: &Disassembler,
242    ) -> Result<Vec<Instruction>> {
243        // Find the section containing this function
244        for section in binary.sections() {
245            let start = section.address;
246            let end = start + section.size;
247
248            if function.start_address >= start && function.start_address < end {
249                let data = section.data.as_ref().ok_or_else(|| {
250                    BinaryError::invalid_data("Section data not available for disassembly")
251                })?;
252
253                let offset = (function.start_address - start) as usize;
254                if offset >= data.len() {
255                    return Ok(Vec::new());
256                }
257
258                let available = data.len() - offset;
259                let length = std::cmp::min(function.size as usize, available);
260                if length == 0 {
261                    return Ok(Vec::new());
262                }
263
264                let slice = &data[offset..offset + length];
265                return disassembler.disassemble_at(slice, function.start_address, length);
266            }
267        }
268
269        Ok(Vec::new())
270    }
271
272    /// Analyze a single instruction for call patterns
273    fn analyze_call_instruction(
274        &self,
275        instruction: &Instruction,
276        caller_address: u64,
277        address_to_node: &HashMap<u64, usize>,
278    ) -> Option<CallGraphEdge> {
279        match &instruction.flow {
280            crate::types::ControlFlow::Call(target_address) => {
281                // Direct call
282                if address_to_node.contains_key(target_address) {
283                    let call_site = CallSite {
284                        address: instruction.address,
285                        instruction_bytes: instruction.bytes.clone(),
286                        context: CallContext::Normal,
287                    };
288
289                    let call_type = if *target_address == caller_address {
290                        CallType::Recursive
291                    } else {
292                        CallType::Direct
293                    };
294
295                    return Some(CallGraphEdge {
296                        caller: caller_address,
297                        callee: *target_address,
298                        call_type,
299                        call_sites: vec![call_site],
300                    });
301                }
302            }
303            _ => {
304                // Check for indirect calls if enabled
305                if self.config.analyze_indirect_calls {
306                    if let Some(edge) = self.analyze_indirect_call(instruction, caller_address) {
307                        return Some(edge);
308                    }
309                }
310            }
311        }
312
313        None
314    }
315
316    /// Analyze indirect call patterns
317    fn analyze_indirect_call(
318        &self,
319        instruction: &Instruction,
320        caller_address: u64,
321    ) -> Option<CallGraphEdge> {
322        // Detect indirect call patterns (simplified)
323        if instruction.mnemonic.starts_with("call") && instruction.operands.contains('[') {
324            // This is an indirect call through memory or register
325            let call_site = CallSite {
326                address: instruction.address,
327                instruction_bytes: instruction.bytes.clone(),
328                context: CallContext::Normal,
329            };
330
331            // For indirect calls, we can't determine the exact target at static analysis time
332            // In a real implementation, this would require more sophisticated analysis
333            return Some(CallGraphEdge {
334                caller: caller_address,
335                callee: 0, // Unknown target
336                call_type: CallType::Indirect,
337                call_sites: vec![call_site],
338            });
339        }
340
341        None
342    }
343
344    /// Detect tail call optimizations
345    fn detect_tail_calls(
346        &self,
347        binary: &BinaryFile,
348        functions: &[Function],
349        address_to_node: &HashMap<u64, usize>,
350    ) -> Result<Vec<CallGraphEdge>> {
351        let mut tail_call_edges = Vec::new();
352        let disassembler = Disassembler::new(binary.architecture())?;
353
354        for function in functions {
355            if let Ok(instructions) =
356                self.get_function_instructions(binary, function, &disassembler)
357            {
358                // Look for jump instructions at the end of functions that target other functions
359                if let Some(last_instruction) = instructions.last() {
360                    if let crate::types::ControlFlow::Jump(target) = &last_instruction.flow {
361                        if address_to_node.contains_key(target) && *target != function.start_address
362                        {
363                            let call_site = CallSite {
364                                address: last_instruction.address,
365                                instruction_bytes: last_instruction.bytes.clone(),
366                                context: CallContext::Normal,
367                            };
368
369                            tail_call_edges.push(CallGraphEdge {
370                                caller: function.start_address,
371                                callee: *target,
372                                call_type: CallType::TailCall,
373                                call_sites: vec![call_site],
374                            });
375                        }
376                    }
377                }
378            }
379        }
380
381        Ok(tail_call_edges)
382    }
383
384    /// Update node in-degree and out-degree based on edges
385    fn update_node_degrees(&self, nodes: &mut [CallGraphNode], edges: &[CallGraphEdge]) {
386        // Count degrees
387        let mut in_degrees: HashMap<u64, u32> = HashMap::new();
388        let mut out_degrees: HashMap<u64, u32> = HashMap::new();
389
390        for edge in edges {
391            *out_degrees.entry(edge.caller).or_insert(0) += 1;
392            *in_degrees.entry(edge.callee).or_insert(0) += 1;
393        }
394
395        // Update nodes
396        for node in nodes {
397            node.in_degree = in_degrees.get(&node.function_address).copied().unwrap_or(0);
398            node.out_degree = out_degrees
399                .get(&node.function_address)
400                .copied()
401                .unwrap_or(0);
402        }
403    }
404
405    /// Detect recursive functions
406    fn detect_recursion(&self, nodes: &mut [CallGraphNode], edges: &[CallGraphEdge]) {
407        let mut recursive_functions = HashSet::new();
408
409        // Direct recursion
410        for edge in edges {
411            if edge.caller == edge.callee {
412                recursive_functions.insert(edge.caller);
413            }
414        }
415
416        // Indirect recursion using DFS
417        let mut adjacency: HashMap<u64, Vec<u64>> = HashMap::new();
418        for edge in edges {
419            adjacency.entry(edge.caller).or_default().push(edge.callee);
420        }
421
422        for node in nodes.iter() {
423            if self.has_cycle_from_node(node.function_address, &adjacency) {
424                recursive_functions.insert(node.function_address);
425            }
426        }
427
428        // Update nodes
429        for node in nodes {
430            node.is_recursive = recursive_functions.contains(&node.function_address);
431        }
432    }
433
434    /// Check if there's a cycle starting from a specific node
435    fn has_cycle_from_node(&self, start: u64, adjacency: &HashMap<u64, Vec<u64>>) -> bool {
436        let mut visited = HashSet::new();
437        let mut rec_stack = HashSet::new();
438        self.dfs_has_cycle(start, &mut visited, &mut rec_stack, adjacency)
439    }
440
441    /// DFS helper for cycle detection
442    #[allow(clippy::only_used_in_recursion)]
443    fn dfs_has_cycle(
444        &self,
445        node: u64,
446        visited: &mut HashSet<u64>,
447        rec_stack: &mut HashSet<u64>,
448        adjacency: &HashMap<u64, Vec<u64>>,
449    ) -> bool {
450        visited.insert(node);
451        rec_stack.insert(node);
452
453        if let Some(neighbors) = adjacency.get(&node) {
454            for &neighbor in neighbors {
455                if !visited.contains(&neighbor) {
456                    if self.dfs_has_cycle(neighbor, visited, rec_stack, adjacency) {
457                        return true;
458                    }
459                } else if rec_stack.contains(&neighbor) {
460                    return true;
461                }
462            }
463        }
464
465        rec_stack.remove(&node);
466        false
467    }
468
469    /// Find entry points in the call graph
470    fn find_entry_points(&self, binary: &BinaryFile, functions: &[Function]) -> Vec<u64> {
471        let mut entry_points = Vec::new();
472
473        // Add main entry point
474        if let Some(entry) = binary.entry_point() {
475            entry_points.push(entry);
476        }
477
478        // Add other known entry points
479        for function in functions {
480            match function.function_type {
481                crate::types::FunctionType::Entrypoint | crate::types::FunctionType::Main => {
482                    if !entry_points.contains(&function.start_address) {
483                        entry_points.push(function.start_address);
484                    }
485                }
486                _ => {}
487            }
488        }
489
490        entry_points
491    }
492
493    /// Compute call depths from entry points using BFS
494    fn compute_call_depths(&self, call_graph: &mut CallGraph) -> Result<()> {
495        let mut address_to_node: HashMap<u64, usize> = HashMap::new();
496        for (i, node) in call_graph.nodes.iter().enumerate() {
497            address_to_node.insert(node.function_address, i);
498        }
499
500        // Build adjacency list
501        let mut adjacency: HashMap<u64, Vec<u64>> = HashMap::new();
502        for edge in &call_graph.edges {
503            adjacency.entry(edge.caller).or_default().push(edge.callee);
504        }
505
506        // BFS from each entry point
507        for &entry_point in &call_graph.entry_points {
508            let mut queue = VecDeque::new();
509            let mut visited = HashSet::new();
510
511            queue.push_back((entry_point, 0));
512            visited.insert(entry_point);
513
514            while let Some((current_addr, depth)) = queue.pop_front() {
515                if let Some(&node_index) = address_to_node.get(&current_addr) {
516                    // Update call depth if not set or if we found a shorter path
517                    let current_depth = call_graph.nodes[node_index].call_depth;
518                    if current_depth.is_none() || current_depth.unwrap() > depth {
519                        call_graph.nodes[node_index].call_depth = Some(depth);
520                    }
521                }
522
523                // Add neighbors to queue
524                if let Some(neighbors) = adjacency.get(&current_addr) {
525                    for &neighbor in neighbors {
526                        if !visited.contains(&neighbor) {
527                            visited.insert(neighbor);
528                            queue.push_back((neighbor, depth + 1));
529                        }
530                    }
531                }
532            }
533        }
534
535        Ok(())
536    }
537
538    /// Find unreachable functions
539    fn find_unreachable_functions(&self, call_graph: &CallGraph) -> Vec<u64> {
540        let reachable: HashSet<u64> = call_graph
541            .nodes
542            .iter()
543            .filter(|node| node.call_depth.is_some())
544            .map(|node| node.function_address)
545            .collect();
546
547        call_graph
548            .nodes
549            .iter()
550            .filter(|node| !reachable.contains(&node.function_address))
551            .map(|node| node.function_address)
552            .collect()
553    }
554
555    /// Compute call graph statistics
556    fn compute_statistics(&self, call_graph: &CallGraph) -> CallGraphStatistics {
557        let total_functions = call_graph.nodes.len();
558        let total_calls = call_graph.edges.len();
559
560        let direct_calls = call_graph
561            .edges
562            .iter()
563            .filter(|edge| matches!(edge.call_type, CallType::Direct))
564            .count();
565
566        let indirect_calls = call_graph
567            .edges
568            .iter()
569            .filter(|edge| matches!(edge.call_type, CallType::Indirect))
570            .count();
571
572        let recursive_functions = call_graph
573            .nodes
574            .iter()
575            .filter(|node| node.is_recursive)
576            .count();
577
578        let leaf_functions = call_graph
579            .nodes
580            .iter()
581            .filter(|node| node.out_degree == 0)
582            .count();
583
584        let entry_points = call_graph.entry_points.len();
585        let unreachable_functions = call_graph.unreachable_functions.len();
586
587        let max_call_depth = call_graph
588            .nodes
589            .iter()
590            .filter_map(|node| node.call_depth)
591            .max()
592            .unwrap_or(0);
593
594        let depths: Vec<u32> = call_graph
595            .nodes
596            .iter()
597            .filter_map(|node| node.call_depth)
598            .collect();
599
600        let average_call_depth = if !depths.is_empty() {
601            depths.iter().sum::<u32>() as f64 / depths.len() as f64
602        } else {
603            0.0
604        };
605
606        let cyclic_dependencies = self.count_cyclic_dependencies(call_graph);
607
608        CallGraphStatistics {
609            total_functions,
610            total_calls,
611            direct_calls,
612            indirect_calls,
613            recursive_functions,
614            leaf_functions,
615            entry_points,
616            unreachable_functions,
617            max_call_depth,
618            average_call_depth,
619            cyclic_dependencies,
620        }
621    }
622
623    /// Count strongly connected components (cyclic dependencies)
624    fn count_cyclic_dependencies(&self, call_graph: &CallGraph) -> usize {
625        // Simplified cycle counting - counts functions involved in any cycle
626        call_graph
627            .nodes
628            .iter()
629            .filter(|node| node.is_recursive)
630            .count()
631    }
632}
633
634/// Configuration for DOT export
635#[derive(Debug, Clone)]
636pub struct DotConfig {
637    /// Include function addresses in labels
638    pub include_addresses: bool,
639    /// Color nodes by type
640    pub color_by_type: bool,
641    /// Show call counts on edges
642    pub show_call_counts: bool,
643    /// Cluster nodes by module
644    pub cluster_by_module: bool,
645    /// Maximum number of nodes to include
646    pub max_nodes: Option<usize>,
647}
648
649impl Default for DotConfig {
650    fn default() -> Self {
651        Self {
652            include_addresses: true,
653            color_by_type: true,
654            show_call_counts: false,
655            cluster_by_module: false,
656            max_nodes: Some(1000),
657        }
658    }
659}
660
661/// Trait for call graph exporters
662pub trait CallGraphExporter {
663    /// Export call graph to string format
664    fn export(&self, graph: &CallGraph) -> Result<String>;
665}
666
667/// DOT format exporter for Graphviz visualization
668pub struct DotExporter {
669    config: DotConfig,
670}
671
672impl DotExporter {
673    /// Create new DOT exporter with configuration
674    pub fn new(config: DotConfig) -> Self {
675        Self { config }
676    }
677
678    /// Create new DOT exporter with default configuration
679    pub fn new_default() -> Self {
680        Self {
681            config: DotConfig::default(),
682        }
683    }
684}
685
686impl CallGraphExporter for DotExporter {
687    fn export(&self, graph: &CallGraph) -> Result<String> {
688        let mut dot = String::new();
689
690        // DOT header
691        dot.push_str("digraph CallGraph {\n");
692        dot.push_str("  rankdir=TB;\n");
693        dot.push_str("  node [shape=box, style=filled];\n");
694        dot.push_str("  edge [arrowhead=normal];\n\n");
695
696        // Limit nodes if configured
697        let nodes_to_include = if let Some(max_nodes) = self.config.max_nodes {
698            &graph.nodes[..std::cmp::min(max_nodes, graph.nodes.len())]
699        } else {
700            &graph.nodes
701        };
702
703        // Export nodes
704        for node in nodes_to_include {
705            let label = if self.config.include_addresses {
706                format!("{}\\n0x{:x}", node.function_name, node.function_address)
707            } else {
708                node.function_name.clone()
709            };
710
711            let color = if self.config.color_by_type {
712                match node.node_type {
713                    NodeType::EntryPoint => "lightgreen",
714                    NodeType::Library => "lightblue",
715                    NodeType::External => "lightyellow",
716                    NodeType::Internal => "lightgray",
717                    NodeType::Indirect => "orange",
718                    NodeType::Virtual => "purple",
719                    NodeType::Unknown => "pink",
720                }
721            } else {
722                "lightgray"
723            };
724
725            dot.push_str(&format!(
726                "  \"0x{:x}\" [label=\"{}\", fillcolor=\"{}\"];\n",
727                node.function_address, label, color
728            ));
729        }
730
731        dot.push('\n');
732
733        // Export edges
734        let node_addresses: HashSet<u64> = nodes_to_include
735            .iter()
736            .map(|n| n.function_address)
737            .collect();
738
739        for edge in &graph.edges {
740            // Only include edges between included nodes
741            if node_addresses.contains(&edge.caller) && node_addresses.contains(&edge.callee) {
742                let style = match edge.call_type {
743                    CallType::Direct => "",
744                    CallType::Indirect => ", style=dashed",
745                    CallType::TailCall => ", color=red",
746                    CallType::Virtual => ", color=purple",
747                    CallType::Recursive => ", color=green, style=bold",
748                    CallType::Conditional => ", color=orange",
749                };
750
751                let label = if self.config.show_call_counts {
752                    format!(" [label=\"{}\"{}", edge.call_sites.len(), style)
753                } else {
754                    format!(" [{}]", &style[2..]) // Remove leading ", "
755                };
756
757                dot.push_str(&format!(
758                    "  \"0x{:x}\" -> \"0x{:x}\"{};\n",
759                    edge.caller, edge.callee, label
760                ));
761            }
762        }
763
764        dot.push_str("}\n");
765        Ok(dot)
766    }
767}
768
769/// JSON exporter for programmatic analysis
770pub struct JsonExporter;
771
772impl CallGraphExporter for JsonExporter {
773    fn export(&self, graph: &CallGraph) -> Result<String> {
774        #[cfg(feature = "serde-support")]
775        {
776            serde_json::to_string_pretty(graph)
777                .map_err(|e| BinaryError::invalid_data(format!("JSON serialization failed: {}", e)))
778        }
779        #[cfg(not(feature = "serde-support"))]
780        {
781            let _ = graph; // Suppress unused warning
782            Err(BinaryError::invalid_data(
783                "JSON export requires 'serde-support' feature",
784            ))
785        }
786    }
787}
788
789impl CallGraph {
790    /// Export call graph to DOT format for Graphviz
791    pub fn to_dot(&self) -> String {
792        let exporter = DotExporter::new_default();
793        exporter.export(self).unwrap_or_default()
794    }
795
796    /// Export call graph to DOT format with custom configuration
797    pub fn to_dot_with_config(&self, config: DotConfig) -> String {
798        let exporter = DotExporter::new(config);
799        exporter.export(self).unwrap_or_default()
800    }
801
802    /// Export call graph to JSON format
803    pub fn to_json(&self) -> String {
804        let exporter = JsonExporter;
805        exporter.export(self).unwrap_or_default()
806    }
807
808    /// Detect cycles in the call graph
809    pub fn detect_cycles(&self) -> Vec<Vec<u64>> {
810        let mut cycles = Vec::new();
811        let mut visited = HashSet::new();
812        let mut rec_stack = HashSet::new();
813        let mut current_path = Vec::new();
814
815        // Build adjacency list
816        let mut adjacency: HashMap<u64, Vec<u64>> = HashMap::new();
817        for edge in &self.edges {
818            adjacency.entry(edge.caller).or_default().push(edge.callee);
819        }
820
821        // DFS from each unvisited node
822        for node in &self.nodes {
823            if !visited.contains(&node.function_address) {
824                self.dfs_find_cycles(
825                    node.function_address,
826                    &mut visited,
827                    &mut rec_stack,
828                    &mut current_path,
829                    &mut cycles,
830                    &adjacency,
831                );
832            }
833        }
834
835        cycles
836    }
837
838    /// DFS helper for finding cycles
839    #[allow(clippy::only_used_in_recursion)]
840    fn dfs_find_cycles(
841        &self,
842        node: u64,
843        visited: &mut HashSet<u64>,
844        rec_stack: &mut HashSet<u64>,
845        current_path: &mut Vec<u64>,
846        cycles: &mut Vec<Vec<u64>>,
847        adjacency: &HashMap<u64, Vec<u64>>,
848    ) {
849        visited.insert(node);
850        rec_stack.insert(node);
851        current_path.push(node);
852
853        if let Some(neighbors) = adjacency.get(&node) {
854            for &neighbor in neighbors {
855                if !visited.contains(&neighbor) {
856                    self.dfs_find_cycles(
857                        neighbor,
858                        visited,
859                        rec_stack,
860                        current_path,
861                        cycles,
862                        adjacency,
863                    );
864                } else if rec_stack.contains(&neighbor) {
865                    // Found a cycle - extract the cycle path
866                    if let Some(cycle_start) = current_path.iter().position(|&x| x == neighbor) {
867                        let cycle = current_path[cycle_start..].to_vec();
868                        cycles.push(cycle);
869                    }
870                }
871            }
872        }
873
874        current_path.pop();
875        rec_stack.remove(&node);
876    }
877}
878
879/// Analyze binary call graph
880pub fn analyze_binary(binary: &BinaryFile) -> Result<CallGraph> {
881    let analyzer = CallGraphAnalyzer::new_default();
882    analyzer.analyze_binary(binary)
883}
884
885/// Analyze binary call graph with custom configuration
886pub fn analyze_binary_with_config(
887    binary: &BinaryFile,
888    config: CallGraphConfig,
889) -> Result<CallGraph> {
890    let analyzer = CallGraphAnalyzer::new(config);
891    analyzer.analyze_binary(binary)
892}
893
894#[cfg(test)]
895mod tests {
896    use super::*;
897    use crate::types::*;
898
899    #[test]
900    fn test_analyzer_creation() {
901        let analyzer = CallGraphAnalyzer::new_default();
902        assert!(analyzer.config.analyze_indirect_calls);
903        assert!(analyzer.config.detect_tail_calls);
904    }
905
906    #[test]
907    fn test_library_function_detection() {
908        let analyzer = CallGraphAnalyzer::new_default();
909        assert!(analyzer.is_library_function("printf"));
910        assert!(analyzer.is_library_function("libc_start_main"));
911        assert!(analyzer.is_library_function("__stack_chk_fail"));
912        assert!(!analyzer.is_library_function("user_function"));
913        assert!(!analyzer.is_library_function("main"));
914    }
915
916    #[test]
917    fn test_node_type_classification() {
918        let analyzer = CallGraphAnalyzer::new_default();
919
920        let entry_function = Function {
921            name: "_start".to_string(),
922            start_address: 0x1000,
923            end_address: 0x1100,
924            size: 256,
925            function_type: FunctionType::Entrypoint,
926            calling_convention: None,
927            parameters: Vec::new(),
928            return_type: None,
929        };
930
931        // Create a minimal ELF binary for testing (minimal header)
932        let elf_data = vec![
933            0x7f, 0x45, 0x4c, 0x46, // ELF magic
934            0x02, // 64-bit
935            0x01, // Little endian
936            0x01, // Version
937            0x00, // System V ABI
938            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Padding
939            0x02, 0x00, // Executable file
940            0x3e, 0x00, // x86-64
941        ];
942
943        // For this test, we don't need a fully valid binary, just enough to classify node types
944        // The actual binary parsing might fail, but we can still test the logic
945        match crate::BinaryFile::parse(&elf_data) {
946            Ok(binary) => {
947                let node_type = analyzer.classify_node_type(&entry_function, &binary);
948                assert_eq!(node_type, NodeType::EntryPoint);
949            }
950            Err(_) => {
951                // If parsing fails (which is expected for minimal data), just test the function type detection
952                // This is acceptable since we're primarily testing the classification logic
953                assert_eq!(entry_function.function_type, FunctionType::Entrypoint);
954            }
955        }
956    }
957}