1use crate::{
8 BinaryError, BinaryFile, Result,
9 disasm::{Disassembler, DisassemblyConfig, section_file_data},
10 types::{
11 Architecture, BasicBlock, ComplexityMetrics, ControlFlow as FlowType, ControlFlowGraph,
12 Function, Instruction,
13 },
14};
15use std::collections::{HashMap, HashSet};
16
17#[derive(Clone)]
23pub struct ControlFlowAnalyzer {
24 #[allow(dead_code)]
26 architecture: Architecture,
27 config: AnalysisConfig,
29}
30
31#[derive(Debug, Clone)]
33pub struct AnalysisConfig {
34 pub max_instructions: usize,
36 pub max_functions: usize,
38 pub max_total_instructions: usize,
40 pub max_loops: usize,
42 pub max_total_loop_body_blocks: usize,
46 pub detect_loops: bool,
48 pub calculate_metrics: bool,
50 pub enable_cognitive_complexity: bool,
52 pub enable_advanced_loops: bool,
54}
55
56impl Default for AnalysisConfig {
57 fn default() -> Self {
58 Self {
59 max_instructions: 10000,
60 max_functions: 10_000,
61 max_total_instructions: 1_000_000,
62 max_loops: 10_000,
63 max_total_loop_body_blocks: 1_000_000,
64 detect_loops: true,
65 calculate_metrics: true,
66 enable_cognitive_complexity: true,
67 enable_advanced_loops: true,
68 }
69 }
70}
71
72#[derive(Clone, Copy)]
73struct LoopBudget {
74 remaining_loops: usize,
75 remaining_body_blocks: usize,
76 configured_max_loops: usize,
77 configured_max_body_blocks: usize,
78}
79
80impl LoopBudget {
81 fn full(config: &AnalysisConfig) -> Self {
82 Self {
83 remaining_loops: config.max_loops,
84 remaining_body_blocks: config.max_total_loop_body_blocks,
85 configured_max_loops: config.max_loops,
86 configured_max_body_blocks: config.max_total_loop_body_blocks,
87 }
88 }
89}
90
91enum FunctionAnalysisFailure {
92 Recoverable(BinaryError),
93 ResourceLimit(BinaryError),
94}
95
96impl FunctionAnalysisFailure {
97 fn into_error(self) -> BinaryError {
98 match self {
99 Self::Recoverable(error) | Self::ResourceLimit(error) => error,
100 }
101 }
102}
103
104struct FunctionAnalysis {
105 graph: ControlFlowGraph,
106 loop_count: usize,
107 loop_body_blocks: usize,
108}
109
110struct DominatorIndex {
111 entry: Vec<usize>,
112 exit: Vec<usize>,
113 nesting_depth: Vec<u32>,
114}
115
116impl DominatorIndex {
117 fn dominates(&self, dominator: usize, block: usize) -> bool {
118 self.entry.get(dominator).is_some_and(|&dominator_entry| {
119 self.entry.get(block).is_some_and(|&block_entry| {
120 dominator_entry <= block_entry && self.exit[block] <= self.exit[dominator]
121 })
122 })
123 }
124}
125
126impl ControlFlowAnalyzer {
127 pub fn new(architecture: Architecture) -> Self {
129 Self {
130 architecture,
131 config: AnalysisConfig::default(),
132 }
133 }
134
135 pub fn with_config(architecture: Architecture, config: AnalysisConfig) -> Self {
137 Self {
138 architecture,
139 config,
140 }
141 }
142
143 pub fn analyze_binary(&self, binary: &BinaryFile) -> Result<Vec<ControlFlowGraph>> {
145 let functions = self.extract_functions(binary)?;
147 let mut cfgs = Vec::new();
148 cfgs.try_reserve(functions.len())
149 .map_err(|error| Self::allocation_error("control-flow result graphs", error))?;
150 let mut total_instructions = 0_usize;
151 let mut total_loops = 0_usize;
152 let mut total_loop_body_blocks = 0_usize;
153 let mut last_error = None;
154
155 for function in &functions {
156 let remaining_instructions = self
157 .config
158 .max_total_instructions
159 .saturating_sub(total_instructions);
160 let decode_limit = self
161 .config
162 .max_instructions
163 .saturating_add(1)
164 .min(remaining_instructions.saturating_add(1));
165
166 match self.get_function_instructions(binary, function, decode_limit) {
167 Ok(instructions) => {
168 total_instructions = self.validate_instruction_count(
169 function,
170 instructions.len(),
171 total_instructions,
172 )?;
173 let loop_budget = LoopBudget {
174 remaining_loops: self.config.max_loops.saturating_sub(total_loops),
175 remaining_body_blocks: self
176 .config
177 .max_total_loop_body_blocks
178 .saturating_sub(total_loop_body_blocks),
179 configured_max_loops: self.config.max_loops,
180 configured_max_body_blocks: self.config.max_total_loop_body_blocks,
181 };
182 match self.analyze_function_instructions(function, &instructions, loop_budget) {
183 Ok(analysis) => {
184 total_loops =
185 total_loops
186 .checked_add(analysis.loop_count)
187 .ok_or_else(|| {
188 BinaryError::control_flow("Loop count overflowed usize")
189 })?;
190 total_loop_body_blocks = total_loop_body_blocks
191 .checked_add(analysis.loop_body_blocks)
192 .ok_or_else(|| {
193 BinaryError::control_flow(
194 "Loop body membership count overflowed usize",
195 )
196 })?;
197 cfgs.push(analysis.graph);
198 }
199 Err(FunctionAnalysisFailure::Recoverable(error)) => {
200 last_error = Some(error);
201 }
202 Err(FunctionAnalysisFailure::ResourceLimit(error)) => return Err(error),
203 }
204 }
205 Err(error) => last_error = Some(error),
206 }
207 }
208
209 if cfgs.is_empty()
210 && let Some(error) = last_error
211 {
212 return Err(error);
213 }
214
215 Ok(cfgs)
216 }
217
218 pub fn analyze_function(
220 &self,
221 binary: &BinaryFile,
222 function: &Function,
223 ) -> Result<ControlFlowGraph> {
224 let decode_limit = self
225 .config
226 .max_instructions
227 .saturating_add(1)
228 .min(self.config.max_total_instructions.saturating_add(1));
229 let instructions = self.get_function_instructions(binary, function, decode_limit)?;
230 self.validate_instruction_count(function, instructions.len(), 0)?;
231 self.analyze_function_instructions(function, &instructions, LoopBudget::full(&self.config))
232 .map(|analysis| analysis.graph)
233 .map_err(FunctionAnalysisFailure::into_error)
234 }
235
236 fn analyze_function_instructions(
237 &self,
238 function: &Function,
239 instructions: &[Instruction],
240 loop_budget: LoopBudget,
241 ) -> std::result::Result<FunctionAnalysis, FunctionAnalysisFailure> {
242 let mut basic_blocks = self
244 .build_basic_blocks(instructions)
245 .map_err(FunctionAnalysisFailure::Recoverable)?;
246
247 let mut analyzer = self.clone();
249 let dominator_index = if self.config.enable_advanced_loops || self.config.calculate_metrics
250 {
251 analyzer
252 .build_dominator_tree(&mut basic_blocks)
253 .map_err(FunctionAnalysisFailure::Recoverable)?;
254 Some(
255 Self::build_dominator_index(&basic_blocks)
256 .map_err(FunctionAnalysisFailure::Recoverable)?,
257 )
258 } else {
259 None
260 };
261
262 let complexity = if self.config.calculate_metrics {
264 let dominator_index = dominator_index.as_ref().ok_or_else(|| {
265 FunctionAnalysisFailure::Recoverable(BinaryError::control_flow(
266 "Dominator index was not built for complexity analysis",
267 ))
268 })?;
269 self.calculate_complexity(&basic_blocks, dominator_index)
270 } else {
271 ComplexityMetrics::default()
272 };
273
274 let loops = if self.config.enable_advanced_loops {
276 let dominator_index = dominator_index.as_ref().ok_or_else(|| {
277 FunctionAnalysisFailure::Recoverable(BinaryError::control_flow(
278 "Dominator index was not built for advanced loop analysis",
279 ))
280 })?;
281 analyzer.analyze_loops_from_dominators(
282 &mut basic_blocks,
283 dominator_index,
284 loop_budget,
285 )?
286 } else {
287 Vec::new()
288 };
289 let loop_body_blocks = loops.iter().try_fold(0_usize, |total, loop_info| {
290 total
291 .checked_add(loop_info.body_blocks.len())
292 .ok_or_else(|| {
293 FunctionAnalysisFailure::ResourceLimit(BinaryError::control_flow(
294 "Loop body membership count overflowed usize",
295 ))
296 })
297 })?;
298 let loop_count = loops.len();
299
300 if self.config.enable_advanced_loops {
302 analyzer
303 .classify_block_types(&mut basic_blocks)
304 .map_err(FunctionAnalysisFailure::Recoverable)?;
305 }
306
307 Ok(FunctionAnalysis {
308 graph: ControlFlowGraph {
309 function: function.clone(),
310 basic_blocks,
311 complexity,
312 loops,
313 },
314 loop_count,
315 loop_body_blocks,
316 })
317 }
318
319 fn extract_functions(&self, binary: &BinaryFile) -> Result<Vec<Function>> {
321 let mut functions = Vec::new();
322 let mut seen_addresses = HashSet::new();
323 let reservation = binary
324 .symbols()
325 .len()
326 .min(self.config.max_functions.saturating_add(1));
327 functions
328 .try_reserve(reservation)
329 .map_err(|error| Self::allocation_error("discovered functions", error))?;
330 seen_addresses
331 .try_reserve(reservation)
332 .map_err(|error| Self::allocation_error("function address set", error))?;
333
334 for symbol in binary.symbols() {
335 if matches!(symbol.symbol_type, crate::types::SymbolType::Function)
336 && symbol.size > 0
337 && seen_addresses.insert(symbol.address)
338 {
339 self.ensure_function_limit(functions.len().saturating_add(1))?;
340 let function = Function {
341 name: symbol.name.clone(),
342 start_address: symbol.address,
343 end_address: symbol.address.checked_add(symbol.size).ok_or_else(|| {
344 BinaryError::invalid_data(format!(
345 "Function '{}' address range overflows",
346 symbol.name
347 ))
348 })?,
349 size: symbol.size,
350 function_type: crate::types::FunctionType::Normal,
351 calling_convention: None,
352 parameters: Vec::new(),
353 return_type: None,
354 };
355 functions.push(function);
356 }
357 }
358
359 if let (true, Some(entry_point)) = (functions.is_empty(), binary.entry_point()) {
361 self.ensure_function_limit(1)?;
362 let function = Function {
363 name: "_start".to_string(),
364 start_address: entry_point,
365 end_address: entry_point.checked_add(1000).ok_or_else(|| {
366 BinaryError::invalid_data("Entry-point address range overflows")
367 })?, size: 1000,
369 function_type: crate::types::FunctionType::Entrypoint,
370 calling_convention: None,
371 parameters: Vec::new(),
372 return_type: None,
373 };
374 functions.push(function);
375 }
376
377 functions.sort_by(|left, right| {
378 left.start_address
379 .cmp(&right.start_address)
380 .then_with(|| left.name.cmp(&right.name))
381 });
382 Ok(functions)
383 }
384
385 fn get_function_instructions(
387 &self,
388 binary: &BinaryFile,
389 function: &Function,
390 max_instructions: usize,
391 ) -> Result<Vec<Instruction>> {
392 for section in binary.sections() {
394 let start = section.address;
395 let Some(end) = start.checked_add(section.size) else {
396 continue;
397 };
398
399 if section.permissions.execute
400 && function.start_address >= start
401 && function.start_address < end
402 {
403 let function_size = usize::try_from(function.size).unwrap_or(usize::MAX);
404 let data = section_file_data(
405 binary,
406 section,
407 function.start_address - start,
408 Some(function_size),
409 )?;
410 let length = data.len();
411 if length == 0 {
412 return Ok(Vec::new());
413 }
414
415 let disassembler = Disassembler::with_config(
416 self.architecture,
417 DisassemblyConfig {
418 max_instructions,
419 ..DisassemblyConfig::default()
420 },
421 )?;
422 return disassembler.disassemble_at(data, function.start_address, length);
423 }
424 }
425
426 Err(BinaryError::invalid_data(
427 "Function bytes not found in any executable section",
428 ))
429 }
430
431 fn ensure_function_limit(&self, actual: usize) -> Result<()> {
432 if actual > self.config.max_functions {
433 return Err(BinaryError::control_flow(format!(
434 "Discovered function count {actual} exceeds configured max_functions {}",
435 self.config.max_functions
436 )));
437 }
438 Ok(())
439 }
440
441 fn validate_instruction_count(
442 &self,
443 function: &Function,
444 function_instructions: usize,
445 previous_total: usize,
446 ) -> Result<usize> {
447 if function_instructions > self.config.max_instructions {
448 return Err(BinaryError::control_flow(format!(
449 "Function '{}' decoded {function_instructions} instructions, exceeding configured max_instructions {}",
450 function.name, self.config.max_instructions
451 )));
452 }
453
454 let total = previous_total
455 .checked_add(function_instructions)
456 .ok_or_else(|| BinaryError::control_flow("Total instruction count overflowed usize"))?;
457 if total > self.config.max_total_instructions {
458 return Err(BinaryError::control_flow(format!(
459 "Decoded instruction count {total} exceeds configured max_total_instructions {}",
460 self.config.max_total_instructions
461 )));
462 }
463 Ok(total)
464 }
465
466 fn allocation_error(context: &str, error: std::collections::TryReserveError) -> BinaryError {
467 BinaryError::control_flow(format!("Unable to reserve {context}: {error}"))
468 }
469
470 fn build_basic_blocks(&self, instructions: &[Instruction]) -> Result<Vec<BasicBlock>> {
472 if instructions.is_empty() {
473 return Ok(Vec::new());
474 }
475
476 let mut basic_blocks = Vec::new();
477 let mut block_starts = HashSet::new();
478
479 block_starts.insert(instructions[0].address);
481
482 for (i, instr) in instructions.iter().enumerate() {
484 match &instr.flow {
485 FlowType::Jump(target) | FlowType::ConditionalJump(target) => {
486 block_starts.insert(*target);
488 if i + 1 < instructions.len() {
489 block_starts.insert(instructions[i + 1].address);
490 }
491 }
492 FlowType::Return | FlowType::Interrupt if i + 1 < instructions.len() => {
493 block_starts.insert(instructions[i + 1].address);
495 }
496 _ => {}
497 }
498 }
499
500 let mut current_block_id = 0;
502 let mut current_block_start = 0;
503
504 for (i, instr) in instructions.iter().enumerate() {
505 if block_starts.contains(&instr.address) && i > current_block_start {
506 let block_instructions = instructions[current_block_start..i].to_vec();
508 let start_addr = instructions[current_block_start].address;
509 let end_addr = instructions[i - 1]
510 .address
511 .checked_add(instructions[i - 1].size as u64)
512 .ok_or_else(|| {
513 BinaryError::invalid_data("Instruction address range overflows")
514 })?;
515
516 basic_blocks.push(BasicBlock {
517 id: current_block_id,
518 start_address: start_addr,
519 end_address: end_addr,
520 instructions: block_instructions,
521 successors: Vec::new(), predecessors: Vec::new(), block_type: crate::types::BlockType::Normal, dominator: None, dominance_frontier: Vec::new(), });
527
528 current_block_id += 1;
529 current_block_start = i;
530 }
531 }
532
533 if current_block_start < instructions.len() {
535 let block_instructions = instructions[current_block_start..].to_vec();
536 let start_addr = instructions[current_block_start].address;
537 let last_instruction = &instructions[instructions.len() - 1];
538 let end_addr = last_instruction
539 .address
540 .checked_add(last_instruction.size as u64)
541 .ok_or_else(|| BinaryError::invalid_data("Instruction address range overflows"))?;
542
543 basic_blocks.push(BasicBlock {
544 id: current_block_id,
545 start_address: start_addr,
546 end_address: end_addr,
547 instructions: block_instructions,
548 successors: Vec::new(),
549 predecessors: Vec::new(),
550 block_type: crate::types::BlockType::Normal, dominator: None, dominance_frontier: Vec::new(), });
554 }
555
556 self.build_cfg_edges(&mut basic_blocks)?;
558
559 Ok(basic_blocks)
560 }
561
562 fn build_cfg_edges(&self, basic_blocks: &mut [BasicBlock]) -> Result<()> {
564 let mut addr_to_block: HashMap<u64, usize> = HashMap::new();
565
566 for (i, block) in basic_blocks.iter().enumerate() {
568 addr_to_block.insert(block.start_address, i);
569 }
570
571 for i in 0..basic_blocks.len() {
573 let block = &basic_blocks[i];
574 if let Some(last_instr) = block.instructions.last() {
575 match &last_instr.flow {
576 FlowType::Sequential => {
577 if i + 1 < basic_blocks.len() {
579 Self::add_cfg_edge(basic_blocks, i, i + 1);
580 }
581 }
582 FlowType::Jump(target) => {
583 if let Some(&target_block) = addr_to_block.get(target) {
585 Self::add_cfg_edge(basic_blocks, i, target_block);
586 }
587 }
588 FlowType::ConditionalJump(target) => {
589 if let Some(&target_block) = addr_to_block.get(target) {
591 Self::add_cfg_edge(basic_blocks, i, target_block);
592 }
593 if i + 1 < basic_blocks.len() {
595 Self::add_cfg_edge(basic_blocks, i, i + 1);
596 }
597 }
598 FlowType::Call(_target) => {
599 if i + 1 < basic_blocks.len() {
601 Self::add_cfg_edge(basic_blocks, i, i + 1);
602 }
603 }
605 FlowType::Return | FlowType::Interrupt => {
606 }
608 FlowType::Unknown => {
609 if i + 1 < basic_blocks.len() {
611 Self::add_cfg_edge(basic_blocks, i, i + 1);
612 }
613 }
614 }
615 }
616 }
617
618 Ok(())
619 }
620
621 fn add_cfg_edge(basic_blocks: &mut [BasicBlock], source: usize, target: usize) {
622 if !basic_blocks[source].successors.contains(&target) {
623 basic_blocks[source].successors.push(target);
624 basic_blocks[target].predecessors.push(source);
625 }
626 }
627
628 fn calculate_complexity(
630 &self,
631 basic_blocks: &[BasicBlock],
632 dominator_index: &DominatorIndex,
633 ) -> ComplexityMetrics {
634 let basic_block_count = u32::try_from(basic_blocks.len()).unwrap_or(u32::MAX);
635 let edge_count = basic_blocks.iter().fold(0_u32, |count, block| {
636 count.saturating_add(u32::try_from(block.successors.len()).unwrap_or(u32::MAX))
637 });
638
639 let cyclomatic_complexity = if basic_block_count > 0 {
642 edge_count
643 .saturating_add(self.weak_component_count(basic_blocks).saturating_mul(2))
644 .saturating_sub(basic_block_count)
645 } else {
646 0
647 };
648
649 let loop_count = self.detect_loops(basic_blocks);
651
652 let nesting_depth = dominator_index
654 .nesting_depth
655 .iter()
656 .copied()
657 .max()
658 .unwrap_or(0);
659
660 let cognitive_complexity = if self.config.enable_cognitive_complexity {
662 self.calculate_cognitive_complexity(basic_blocks, &dominator_index.nesting_depth)
663 } else {
664 0
665 };
666
667 let halstead_metrics = self.calculate_halstead_metrics(basic_blocks);
669
670 let maintainability_index = if let Some(ref halstead) = halstead_metrics {
672 let instruction_count = basic_blocks.iter().fold(0_u32, |count, block| {
673 count.saturating_add(u32::try_from(block.instructions.len()).unwrap_or(u32::MAX))
674 });
675 self.calculate_maintainability_index(halstead, cyclomatic_complexity, instruction_count)
676 } else {
677 None
678 };
679
680 ComplexityMetrics {
681 cyclomatic_complexity,
682 basic_block_count,
683 edge_count,
684 nesting_depth,
685 loop_count,
686 cognitive_complexity,
687 halstead_metrics,
688 maintainability_index,
689 }
690 }
691
692 fn detect_loops(&self, basic_blocks: &[BasicBlock]) -> u32 {
694 if !self.config.detect_loops {
695 return 0;
696 }
697
698 u32::try_from(Self::collect_back_edges(basic_blocks).len()).unwrap_or(u32::MAX)
699 }
700
701 fn collect_back_edges(basic_blocks: &[BasicBlock]) -> Vec<(usize, usize)> {
703 let mut state = vec![0_u8; basic_blocks.len()];
704 let mut back_edges = Vec::new();
705
706 for root in 0..basic_blocks.len() {
707 if state[root] != 0 {
708 continue;
709 }
710
711 state[root] = 1;
712 let mut stack = vec![(root, 0_usize)];
713 while let Some(&(node, next_successor)) = stack.last() {
714 if next_successor == basic_blocks[node].successors.len() {
715 state[node] = 2;
716 stack.pop();
717 continue;
718 }
719
720 if let Some(last) = stack.last_mut() {
721 last.1 += 1;
722 }
723 let successor = basic_blocks[node].successors[next_successor];
724 match state[successor] {
725 0 => {
726 state[successor] = 1;
727 stack.push((successor, 0));
728 }
729 1 => back_edges.push((node, successor)),
730 _ => {}
731 }
732 }
733 }
734
735 back_edges
736 }
737
738 fn weak_component_count(&self, basic_blocks: &[BasicBlock]) -> u32 {
739 let mut visited = vec![false; basic_blocks.len()];
740 let mut components = 0_u32;
741
742 for root in 0..basic_blocks.len() {
743 if visited[root] {
744 continue;
745 }
746 components = components.saturating_add(1);
747 visited[root] = true;
748 let mut stack = vec![root];
749 while let Some(node) = stack.pop() {
750 for neighbor in basic_blocks[node]
751 .successors
752 .iter()
753 .chain(&basic_blocks[node].predecessors)
754 .copied()
755 {
756 if !visited[neighbor] {
757 visited[neighbor] = true;
758 stack.push(neighbor);
759 }
760 }
761 }
762 }
763
764 components
765 }
766
767 fn calculate_cognitive_complexity(
770 &self,
771 basic_blocks: &[BasicBlock],
772 nesting_depths: &[u32],
773 ) -> u32 {
774 let mut complexity = 0_u32;
775 for (index, block) in basic_blocks.iter().enumerate() {
776 if matches!(
777 block
778 .instructions
779 .last()
780 .map(|instruction| &instruction.flow),
781 Some(FlowType::ConditionalJump(_))
782 ) {
783 complexity = complexity.saturating_add(
784 1_u32.saturating_add(nesting_depths.get(index).copied().unwrap_or(0)),
785 );
786 }
787 }
788
789 complexity.saturating_add(
790 u32::try_from(Self::collect_back_edges(basic_blocks).len()).unwrap_or(u32::MAX),
791 )
792 }
793
794 fn calculate_halstead_metrics(
796 &self,
797 basic_blocks: &[BasicBlock],
798 ) -> Option<crate::types::HalsteadMetrics> {
799 let mut operators = HashMap::new();
800 let mut operands = HashMap::new();
801 let mut total_operators = 0_u32;
802 let mut total_operands = 0_u32;
803
804 for block in basic_blocks {
805 for instruction in &block.instructions {
806 let operator_count = operators
808 .entry(instruction.mnemonic.clone())
809 .or_insert(0_u32);
810 *operator_count = operator_count.saturating_add(1);
811 total_operators = total_operators.saturating_add(1);
812
813 if !instruction.operands.is_empty() {
815 for op in instruction.operands.split(',') {
816 let trimmed = op.trim();
817 if !trimmed.is_empty() {
818 let operand_count =
819 operands.entry(trimmed.to_string()).or_insert(0_u32);
820 *operand_count = operand_count.saturating_add(1);
821 total_operands = total_operands.saturating_add(1);
822 }
823 }
824 }
825 }
826 }
827
828 let n1 = u32::try_from(operators.len()).unwrap_or(u32::MAX); let n2 = u32::try_from(operands.len()).unwrap_or(u32::MAX); let capital_n1 = total_operators; let capital_n2 = total_operands; if n1 == 0 && n2 == 0 {
834 return None;
835 }
836
837 let vocabulary = n1.saturating_add(n2);
838 let length = capital_n1.saturating_add(capital_n2);
839 let x_log2_x = |value: u32| {
840 if value == 0 {
841 0.0
842 } else {
843 let value = f64::from(value);
844 value * value.log2()
845 }
846 };
847 let calculated_length = x_log2_x(n1) + x_log2_x(n2);
848 let volume = (length as f64) * (vocabulary as f64).log2();
849 let difficulty = if n2 > 0 {
850 ((n1 as f64) / 2.0) * ((capital_n2 as f64) / (n2 as f64))
851 } else {
852 0.0
853 };
854 let effort = difficulty * volume;
855 let time = effort / 18.0; let bugs = volume / 3000.0; Some(crate::types::HalsteadMetrics {
859 n1,
860 n2,
861 capital_n1,
862 capital_n2,
863 vocabulary,
864 length,
865 calculated_length,
866 volume,
867 difficulty,
868 effort,
869 time,
870 bugs,
871 })
872 }
873
874 fn calculate_maintainability_index(
876 &self,
877 halstead: &crate::types::HalsteadMetrics,
878 cyclomatic_complexity: u32,
879 lines_of_code: u32,
880 ) -> Option<f64> {
881 if halstead.volume <= 0.0 || lines_of_code == 0 {
882 return None;
883 }
884
885 let mi = 171.0
888 - 5.2 * halstead.volume.ln()
889 - 0.23 * (cyclomatic_complexity as f64)
890 - 16.2 * (lines_of_code as f64).ln();
891
892 Some(mi.clamp(0.0, 100.0))
894 }
895
896 pub fn analyze_loops(
898 &mut self,
899 basic_blocks: &mut [BasicBlock],
900 ) -> Result<Vec<crate::types::Loop>> {
901 if !self.config.enable_advanced_loops {
902 return Ok(Vec::new());
903 }
904
905 self.build_dominator_tree(basic_blocks)?;
906 let dominator_index = Self::build_dominator_index(basic_blocks)?;
907 self.analyze_loops_from_dominators(
908 basic_blocks,
909 &dominator_index,
910 LoopBudget::full(&self.config),
911 )
912 .map_err(FunctionAnalysisFailure::into_error)
913 }
914
915 fn analyze_loops_from_dominators(
916 &mut self,
917 basic_blocks: &mut [BasicBlock],
918 dominator_index: &DominatorIndex,
919 budget: LoopBudget,
920 ) -> std::result::Result<Vec<crate::types::Loop>, FunctionAnalysisFailure> {
921 let mut loops = Vec::new();
922 let mut total_body_blocks = 0_usize;
923 let mut back_edges = Self::collect_back_edges(basic_blocks);
924 back_edges.sort_unstable_by_key(|&(tail, head)| (head, tail));
925
926 for (tail, head) in back_edges {
929 if dominator_index.dominates(head, tail) {
930 let extends_existing = loops
931 .last()
932 .is_some_and(|loop_info: &crate::types::Loop| loop_info.header_block == head);
933 if !extends_existing && loops.len() >= budget.remaining_loops {
934 return Err(FunctionAnalysisFailure::ResourceLimit(
935 BinaryError::control_flow(format!(
936 "Advanced loop count exceeds configured max_loops {}",
937 budget.configured_max_loops
938 )),
939 ));
940 }
941
942 let mut loop_info = self.analyze_natural_loop(
943 head,
944 tail,
945 basic_blocks,
946 dominator_index,
947 budget.configured_max_body_blocks,
948 )?;
949
950 if extends_existing {
951 let existing = loops.last_mut().expect("existing loop was checked above");
952 let old_len = existing.body_blocks.len();
953 let merged_len =
954 Self::sorted_union_len(&existing.body_blocks, &loop_info.body_blocks);
955 let added = merged_len.saturating_sub(old_len);
956 let next_total = total_body_blocks.checked_add(added).ok_or_else(|| {
957 FunctionAnalysisFailure::ResourceLimit(BinaryError::control_flow(
958 "Loop body membership count overflowed usize",
959 ))
960 })?;
961 if next_total > budget.remaining_body_blocks {
962 return Err(FunctionAnalysisFailure::ResourceLimit(
963 BinaryError::control_flow(format!(
964 "Advanced loop body membership exceeds configured max_total_loop_body_blocks {}",
965 budget.configured_max_body_blocks
966 )),
967 ));
968 }
969 existing
970 .body_blocks
971 .try_reserve(loop_info.body_blocks.len())
972 .map_err(|error| {
973 FunctionAnalysisFailure::ResourceLimit(Self::allocation_error(
974 "merged loop body blocks",
975 error,
976 ))
977 })?;
978 existing.body_blocks.append(&mut loop_info.body_blocks);
979 existing.body_blocks.sort_unstable();
980 existing.body_blocks.dedup();
981 total_body_blocks = next_total;
982 } else {
983 let next_total = total_body_blocks
984 .checked_add(loop_info.body_blocks.len())
985 .ok_or_else(|| {
986 FunctionAnalysisFailure::ResourceLimit(BinaryError::control_flow(
987 "Loop body membership count overflowed usize",
988 ))
989 })?;
990 if next_total > budget.remaining_body_blocks {
991 return Err(FunctionAnalysisFailure::ResourceLimit(
992 BinaryError::control_flow(format!(
993 "Advanced loop body membership exceeds configured max_total_loop_body_blocks {}",
994 budget.configured_max_body_blocks
995 )),
996 ));
997 }
998 loops.try_reserve(1).map_err(|error| {
999 FunctionAnalysisFailure::ResourceLimit(Self::allocation_error(
1000 "advanced loops",
1001 error,
1002 ))
1003 })?;
1004 loops.push(loop_info);
1005 total_body_blocks = next_total;
1006 }
1007 }
1008 }
1009
1010 for loop_info in &mut loops {
1012 Self::refresh_loop_exits(loop_info, basic_blocks)
1013 .map_err(FunctionAnalysisFailure::ResourceLimit)?;
1014 self.classify_loop_type(loop_info);
1015 self.detect_induction_variables(loop_info, basic_blocks);
1016 }
1017
1018 let mut containing_loops = Vec::new();
1019 containing_loops
1020 .try_reserve_exact(basic_blocks.len())
1021 .map_err(|error| {
1022 FunctionAnalysisFailure::ResourceLimit(Self::allocation_error(
1023 "loop nesting counters",
1024 error,
1025 ))
1026 })?;
1027 containing_loops.resize(basic_blocks.len(), 0_u32);
1028 for loop_info in &loops {
1029 containing_loops[loop_info.header_block] =
1030 containing_loops[loop_info.header_block].saturating_add(1);
1031 for &block in &loop_info.body_blocks {
1032 containing_loops[block] = containing_loops[block].saturating_add(1);
1033 }
1034 }
1035 for loop_info in &mut loops {
1036 loop_info.nesting_level = containing_loops[loop_info.header_block];
1037 }
1038
1039 Ok(loops)
1040 }
1041
1042 fn analyze_natural_loop(
1044 &self,
1045 header: usize,
1046 tail: usize,
1047 basic_blocks: &[BasicBlock],
1048 dominator_index: &DominatorIndex,
1049 max_body_blocks: usize,
1050 ) -> std::result::Result<crate::types::Loop, FunctionAnalysisFailure> {
1051 let mut loop_blocks = HashSet::new();
1052 let mut worklist = Vec::new();
1053 let reservation = basic_blocks.len().min(max_body_blocks.saturating_add(1));
1054 loop_blocks.try_reserve(reservation).map_err(|error| {
1055 FunctionAnalysisFailure::ResourceLimit(Self::allocation_error(
1056 "natural loop block set",
1057 error,
1058 ))
1059 })?;
1060 worklist.try_reserve(reservation).map_err(|error| {
1061 FunctionAnalysisFailure::ResourceLimit(Self::allocation_error(
1062 "natural loop worklist",
1063 error,
1064 ))
1065 })?;
1066
1067 loop_blocks.insert(header);
1069 if tail != header {
1070 loop_blocks.insert(tail);
1071 if max_body_blocks == 0 {
1072 return Err(FunctionAnalysisFailure::ResourceLimit(
1073 BinaryError::control_flow(format!(
1074 "Advanced loop body membership exceeds configured max_total_loop_body_blocks {max_body_blocks}"
1075 )),
1076 ));
1077 }
1078 worklist.push(tail);
1079 }
1080
1081 while let Some(current) = worklist.pop() {
1083 for &pred in &basic_blocks[current].predecessors {
1084 if !loop_blocks.contains(&pred) && dominator_index.dominates(header, pred) {
1085 if loop_blocks.len().saturating_sub(1) >= max_body_blocks {
1086 return Err(FunctionAnalysisFailure::ResourceLimit(
1087 BinaryError::control_flow(format!(
1088 "Advanced loop body membership exceeds configured max_total_loop_body_blocks {max_body_blocks}"
1089 )),
1090 ));
1091 }
1092 loop_blocks.insert(pred);
1093 worklist.push(pred);
1094 }
1095 }
1096 }
1097
1098 let mut body_blocks: Vec<usize> =
1099 loop_blocks.into_iter().filter(|&id| id != header).collect();
1100 body_blocks.sort_unstable();
1101
1102 Ok(crate::types::Loop {
1103 header_block: header,
1104 body_blocks,
1105 exit_blocks: Vec::new(),
1106 loop_type: crate::types::LoopType::Unknown, induction_variables: Vec::new(), is_natural: true, nesting_level: 0, })
1111 }
1112
1113 fn sorted_union_len(left: &[usize], right: &[usize]) -> usize {
1114 let (mut left_index, mut right_index, mut count) = (0_usize, 0_usize, 0_usize);
1115 while left_index < left.len() && right_index < right.len() {
1116 count = count.saturating_add(1);
1117 match left[left_index].cmp(&right[right_index]) {
1118 std::cmp::Ordering::Less => left_index += 1,
1119 std::cmp::Ordering::Greater => right_index += 1,
1120 std::cmp::Ordering::Equal => {
1121 left_index += 1;
1122 right_index += 1;
1123 }
1124 }
1125 }
1126 count
1127 .saturating_add(left.len().saturating_sub(left_index))
1128 .saturating_add(right.len().saturating_sub(right_index))
1129 }
1130
1131 fn refresh_loop_exits(
1132 loop_info: &mut crate::types::Loop,
1133 basic_blocks: &[BasicBlock],
1134 ) -> Result<()> {
1135 let mut blocks = HashSet::new();
1136 blocks
1137 .try_reserve(loop_info.body_blocks.len().saturating_add(1))
1138 .map_err(|error| Self::allocation_error("loop exit block set", error))?;
1139 blocks.insert(loop_info.header_block);
1140 blocks.extend(loop_info.body_blocks.iter().copied());
1141
1142 loop_info.exit_blocks.clear();
1143 for &block in &blocks {
1144 loop_info
1145 .exit_blocks
1146 .try_reserve(basic_blocks[block].successors.len())
1147 .map_err(|error| Self::allocation_error("loop exit blocks", error))?;
1148 loop_info.exit_blocks.extend(
1149 basic_blocks[block]
1150 .successors
1151 .iter()
1152 .copied()
1153 .filter(|successor| !blocks.contains(successor)),
1154 );
1155 }
1156 loop_info.exit_blocks.sort_unstable();
1157 loop_info.exit_blocks.dedup();
1158 Ok(())
1159 }
1160
1161 fn classify_loop_type(&self, loop_info: &mut crate::types::Loop) {
1163 loop_info.loop_type = if loop_info.exit_blocks.is_empty() {
1164 crate::types::LoopType::Infinite
1165 } else if loop_info.is_natural {
1166 crate::types::LoopType::Natural
1167 } else {
1168 crate::types::LoopType::Irreducible
1169 };
1170 }
1171
1172 fn detect_induction_variables(
1174 &self,
1175 loop_info: &mut crate::types::Loop,
1176 basic_blocks: &[BasicBlock],
1177 ) {
1178 let mut induction_vars = HashSet::new();
1179
1180 for block_id in
1182 std::iter::once(loop_info.header_block).chain(loop_info.body_blocks.iter().copied())
1183 {
1184 let block = &basic_blocks[block_id];
1185 for instruction in &block.instructions {
1186 match instruction.mnemonic.as_str() {
1187 "inc" | "dec" | "add" | "sub" if !instruction.operands.is_empty() => {
1188 let operand = instruction.operands.split(',').next().unwrap_or("").trim();
1190 if !operand.is_empty()
1191 && !operand.starts_with('#')
1192 && !operand.starts_with('$')
1193 {
1194 induction_vars.insert(operand.to_string());
1195 }
1196 }
1197 _ => {}
1198 }
1199 }
1200 }
1201
1202 loop_info.induction_variables = induction_vars.into_iter().collect();
1203 loop_info.induction_variables.sort_unstable();
1204 }
1205
1206 pub fn build_dominator_tree(&mut self, basic_blocks: &mut [BasicBlock]) -> Result<()> {
1208 if basic_blocks.is_empty() {
1209 return Ok(());
1210 }
1211 Self::validate_cfg(basic_blocks)?;
1212
1213 let n = basic_blocks.len();
1214 let reverse_postorder = Self::reverse_postorder(basic_blocks);
1215 let mut rpo_index = vec![usize::MAX; n];
1216 for (index, &block) in reverse_postorder.iter().enumerate() {
1217 rpo_index[block] = index;
1218 }
1219 let mut dominators = vec![None; n];
1220 dominators[0] = Some(0); let mut changed = true;
1224 while changed {
1225 changed = false;
1226 for &block in reverse_postorder.iter().skip(1) {
1227 let mut processed_predecessors = basic_blocks[block]
1228 .predecessors
1229 .iter()
1230 .copied()
1231 .filter(|&pred| dominators[pred].is_some());
1232 let Some(mut new_dominator) = processed_predecessors.next() else {
1233 continue;
1234 };
1235
1236 for predecessor in processed_predecessors {
1237 new_dominator = Self::intersect_dominators(
1238 new_dominator,
1239 predecessor,
1240 &dominators,
1241 &rpo_index,
1242 )
1243 .ok_or_else(|| {
1244 BinaryError::invalid_data("Malformed dominator predecessor chain")
1245 })?;
1246 }
1247
1248 if dominators[block] != Some(new_dominator) {
1249 dominators[block] = Some(new_dominator);
1250 changed = true;
1251 }
1252 }
1253 }
1254
1255 for (i, &dom) in dominators.iter().enumerate() {
1257 basic_blocks[i].dominator = dom;
1258 }
1259
1260 Ok(())
1261 }
1262
1263 fn build_dominator_index(basic_blocks: &[BasicBlock]) -> Result<DominatorIndex> {
1266 let block_count = basic_blocks.len();
1267 let mut first_child = vec![None; block_count];
1268 let mut next_sibling = vec![None; block_count];
1269 let mut roots = Vec::new();
1270 roots
1271 .try_reserve(block_count)
1272 .map_err(|error| Self::allocation_error("dominator roots", error))?;
1273
1274 for (block, basic_block) in basic_blocks.iter().enumerate() {
1275 match basic_block.dominator {
1276 None => roots.push(block),
1277 Some(parent) if parent == block => roots.push(block),
1278 Some(parent) => {
1279 if parent >= block_count {
1280 return Err(BinaryError::invalid_data(format!(
1281 "Basic block {block} has out-of-range dominator {parent}"
1282 )));
1283 }
1284 next_sibling[block] = first_child[parent];
1285 first_child[parent] = Some(block);
1286 }
1287 }
1288 }
1289
1290 let mut entry = vec![usize::MAX; block_count];
1291 let mut exit = vec![usize::MAX; block_count];
1292 let mut nesting_depth = vec![0_u32; block_count];
1293 let mut stack = Vec::new();
1294 stack
1295 .try_reserve(block_count.saturating_mul(2))
1296 .map_err(|error| Self::allocation_error("dominator traversal", error))?;
1297 let mut clock = 0_usize;
1298
1299 for root in roots {
1300 if entry[root] != usize::MAX {
1301 continue;
1302 }
1303 stack.push((root, false));
1304 while let Some((block, exiting)) = stack.pop() {
1305 if exiting {
1306 exit[block] = clock;
1307 clock = clock.saturating_add(1);
1308 continue;
1309 }
1310 if entry[block] != usize::MAX {
1311 return Err(BinaryError::invalid_data(
1312 "Immediate-dominator relation contains a cycle",
1313 ));
1314 }
1315
1316 entry[block] = clock;
1317 clock = clock.saturating_add(1);
1318 if let Some(parent) = basic_blocks[block]
1319 .dominator
1320 .filter(|&parent| parent != block)
1321 {
1322 nesting_depth[block] = nesting_depth[parent]
1323 .saturating_add(u32::from(basic_blocks[parent].successors.len() > 1));
1324 }
1325
1326 stack.push((block, true));
1327 let mut child = first_child[block];
1328 while let Some(child_block) = child {
1329 stack.push((child_block, false));
1330 child = next_sibling[child_block];
1331 }
1332 }
1333 }
1334
1335 if entry.contains(&usize::MAX) {
1336 return Err(BinaryError::invalid_data(
1337 "Immediate-dominator relation contains a rootless cycle",
1338 ));
1339 }
1340
1341 Ok(DominatorIndex {
1342 entry,
1343 exit,
1344 nesting_depth,
1345 })
1346 }
1347
1348 fn intersect_dominators(
1350 mut b1: usize,
1351 mut b2: usize,
1352 dominators: &[Option<usize>],
1353 rpo_index: &[usize],
1354 ) -> Option<usize> {
1355 while b1 != b2 {
1356 while rpo_index[b1] > rpo_index[b2] {
1357 b1 = dominators[b1]?;
1358 }
1359 while rpo_index[b2] > rpo_index[b1] {
1360 b2 = dominators[b2]?;
1361 }
1362 }
1363 Some(b1)
1364 }
1365
1366 fn reverse_postorder(basic_blocks: &[BasicBlock]) -> Vec<usize> {
1367 let mut visited = vec![false; basic_blocks.len()];
1368 let mut postorder = Vec::with_capacity(basic_blocks.len());
1369 visited[0] = true;
1370 let mut stack = vec![(0_usize, 0_usize)];
1371
1372 while let Some(&(block, successor_index)) = stack.last() {
1373 if successor_index == basic_blocks[block].successors.len() {
1374 postorder.push(block);
1375 stack.pop();
1376 continue;
1377 }
1378
1379 if let Some(last) = stack.last_mut() {
1380 last.1 += 1;
1381 }
1382 let successor = basic_blocks[block].successors[successor_index];
1383 if !visited[successor] {
1384 visited[successor] = true;
1385 stack.push((successor, 0));
1386 }
1387 }
1388
1389 postorder.reverse();
1390 postorder
1391 }
1392
1393 fn validate_cfg(basic_blocks: &[BasicBlock]) -> Result<()> {
1394 let mut successor_edges = HashSet::new();
1395 let mut predecessor_edges = HashSet::new();
1396 for (source, block) in basic_blocks.iter().enumerate() {
1397 for &target in &block.successors {
1398 if basic_blocks.get(target).is_none() {
1399 return Err(BinaryError::invalid_data(format!(
1400 "Basic block {source} has out-of-range successor {target}"
1401 )));
1402 }
1403 if !successor_edges.insert((source, target)) {
1404 return Err(BinaryError::invalid_data(format!(
1405 "CFG contains duplicate successor edge {source} -> {target}"
1406 )));
1407 }
1408 }
1409 for &predecessor in &block.predecessors {
1410 if basic_blocks.get(predecessor).is_none() {
1411 return Err(BinaryError::invalid_data(format!(
1412 "Basic block {source} has out-of-range predecessor {predecessor}"
1413 )));
1414 }
1415 if !predecessor_edges.insert((predecessor, source)) {
1416 return Err(BinaryError::invalid_data(format!(
1417 "CFG contains duplicate predecessor edge {predecessor} -> {source}"
1418 )));
1419 }
1420 }
1421 }
1422 if successor_edges != predecessor_edges {
1423 return Err(BinaryError::invalid_data(
1424 "CFG successor and predecessor edge sets are inconsistent",
1425 ));
1426 }
1427 Ok(())
1428 }
1429
1430 pub fn classify_block_types(&mut self, basic_blocks: &mut [BasicBlock]) -> Result<()> {
1432 if basic_blocks.is_empty() {
1433 return Ok(());
1434 }
1435 Self::validate_cfg(basic_blocks)?;
1436
1437 basic_blocks[0].block_type = crate::types::BlockType::Entry;
1439
1440 #[allow(clippy::needless_range_loop)]
1442 for i in 1..basic_blocks.len() {
1443 let block = &basic_blocks[i];
1444
1445 if block.successors.is_empty() {
1447 basic_blocks[i].block_type = crate::types::BlockType::Exit;
1448 continue;
1449 }
1450
1451 if let Some(last_instruction) = block.instructions.last() {
1453 match last_instruction.flow {
1454 FlowType::Return => {
1455 basic_blocks[i].block_type = crate::types::BlockType::Return;
1456 continue;
1457 }
1458 FlowType::Call(_) => {
1459 basic_blocks[i].block_type = crate::types::BlockType::Call;
1460 continue;
1461 }
1462 FlowType::ConditionalJump(_) => {
1463 basic_blocks[i].block_type = crate::types::BlockType::Conditional;
1464 continue;
1465 }
1466 _ => {}
1467 }
1468 }
1469
1470 basic_blocks[i].block_type = crate::types::BlockType::Normal;
1472 }
1473
1474 Ok(())
1475 }
1476}
1477
1478pub fn analyze_binary(binary: &BinaryFile) -> Result<Vec<ControlFlowGraph>> {
1480 let analyzer = ControlFlowAnalyzer::new(binary.architecture());
1481 analyzer.analyze_binary(binary)
1482}
1483
1484pub fn analyze_function(binary: &BinaryFile, function: &Function) -> Result<ControlFlowGraph> {
1486 let analyzer = ControlFlowAnalyzer::new(binary.architecture());
1487 analyzer.analyze_function(binary, function)
1488}
1489
1490#[cfg(test)]
1491mod tests {
1492 use super::*;
1493 use crate::types::*;
1494
1495 struct TestFormat {
1496 sections: Vec<Section>,
1497 symbols: Vec<Symbol>,
1498 metadata: BinaryMetadata,
1499 }
1500
1501 impl BinaryFormatTrait for TestFormat {
1502 fn format_type(&self) -> BinaryFormat {
1503 BinaryFormat::Raw
1504 }
1505
1506 fn architecture(&self) -> Architecture {
1507 Architecture::X86_64
1508 }
1509
1510 fn entry_point(&self) -> Option<u64> {
1511 None
1512 }
1513
1514 fn sections(&self) -> &[Section] {
1515 &self.sections
1516 }
1517
1518 fn symbols(&self) -> &[Symbol] {
1519 &self.symbols
1520 }
1521
1522 fn imports(&self) -> &[Import] {
1523 &[]
1524 }
1525
1526 fn exports(&self) -> &[Export] {
1527 &[]
1528 }
1529
1530 fn metadata(&self) -> &BinaryMetadata {
1531 &self.metadata
1532 }
1533 }
1534
1535 fn function_symbol(name: &str, address: u64) -> Symbol {
1536 Symbol {
1537 name: name.to_string(),
1538 demangled_name: None,
1539 address,
1540 size: 1,
1541 symbol_type: SymbolType::Function,
1542 binding: SymbolBinding::Global,
1543 visibility: SymbolVisibility::Default,
1544 section_index: None,
1545 }
1546 }
1547
1548 fn function_analysis_binary(include_valid_function: bool) -> BinaryFile {
1549 let data = vec![0xc3];
1550 let mut sections = vec![Section {
1551 name: ".invalid".to_string(),
1552 address: 0x1000,
1553 size: 1,
1554 offset: 2,
1555 file_size: 1,
1556 permissions: SectionPermissions {
1557 read: true,
1558 write: false,
1559 execute: true,
1560 },
1561 section_type: SectionType::Code,
1562 data: None,
1563 }];
1564 let mut symbols = vec![function_symbol("invalid", 0x1000)];
1565
1566 if include_valid_function {
1567 sections.push(Section {
1568 name: ".valid".to_string(),
1569 address: 0x2000,
1570 size: 1,
1571 offset: 0,
1572 file_size: 1,
1573 permissions: SectionPermissions {
1574 read: true,
1575 write: false,
1576 execute: true,
1577 },
1578 section_type: SectionType::Code,
1579 data: Some(data.clone()),
1580 });
1581 symbols.push(function_symbol("valid", 0x2000));
1582 }
1583
1584 let metadata = BinaryMetadata {
1585 size: data.len(),
1586 format: BinaryFormat::Raw,
1587 architecture: Architecture::X86_64,
1588 entry_point: None,
1589 base_address: None,
1590 timestamp: None,
1591 compiler_info: None,
1592 endian: Endianness::Little,
1593 security_features: SecurityFeatures::default(),
1594 };
1595
1596 BinaryFile {
1597 data,
1598 parsed: Box::new(TestFormat {
1599 sections,
1600 symbols,
1601 metadata,
1602 }),
1603 }
1604 }
1605
1606 fn looping_functions_binary() -> BinaryFile {
1607 let data = vec![0xeb, 0xfe, 0xeb, 0xfe];
1609 let sections = vec![Section {
1610 name: ".text".to_string(),
1611 address: 0x1000,
1612 size: data.len() as u64,
1613 offset: 0,
1614 file_size: data.len() as u64,
1615 permissions: SectionPermissions {
1616 read: true,
1617 write: false,
1618 execute: true,
1619 },
1620 section_type: SectionType::Code,
1621 data: Some(data.clone()),
1622 }];
1623 let symbols = [0x1000, 0x1002]
1624 .into_iter()
1625 .enumerate()
1626 .map(|(index, address)| {
1627 let mut symbol = function_symbol(&format!("loop_{index}"), address);
1628 symbol.size = 2;
1629 symbol
1630 })
1631 .collect();
1632 let metadata = BinaryMetadata {
1633 size: data.len(),
1634 format: BinaryFormat::Raw,
1635 architecture: Architecture::X86_64,
1636 entry_point: None,
1637 base_address: None,
1638 timestamp: None,
1639 compiler_info: None,
1640 endian: Endianness::Little,
1641 security_features: SecurityFeatures::default(),
1642 };
1643
1644 BinaryFile {
1645 data,
1646 parsed: Box::new(TestFormat {
1647 sections,
1648 symbols,
1649 metadata,
1650 }),
1651 }
1652 }
1653
1654 #[test]
1655 fn test_analyzer_creation() {
1656 let analyzer = ControlFlowAnalyzer::new(Architecture::X86_64);
1657 assert_eq!(analyzer.architecture, Architecture::X86_64);
1658 }
1659
1660 #[test]
1661 fn test_config_default() {
1662 let config = AnalysisConfig::default();
1663 assert_eq!(config.max_instructions, 10000);
1664 assert_eq!(config.max_functions, 10_000);
1665 assert_eq!(config.max_total_instructions, 1_000_000);
1666 assert_eq!(config.max_loops, 10_000);
1667 assert_eq!(config.max_total_loop_body_blocks, 1_000_000);
1668 assert!(config.detect_loops);
1669 assert!(config.calculate_metrics);
1670 }
1671
1672 #[test]
1673 fn analyze_binary_returns_error_when_every_function_fails() {
1674 let binary = function_analysis_binary(false);
1675 let analyzer = ControlFlowAnalyzer::new(Architecture::X86_64);
1676
1677 let error = analyzer.analyze_binary(&binary).unwrap_err();
1678
1679 assert!(
1680 matches!(error, BinaryError::InvalidData(message) if message.contains("beyond the end of the file"))
1681 );
1682 }
1683
1684 #[test]
1685 fn analyze_binary_preserves_partial_function_success() {
1686 let binary = function_analysis_binary(true);
1687 let analyzer = ControlFlowAnalyzer::with_config(
1688 Architecture::X86_64,
1689 AnalysisConfig {
1690 max_instructions: 1,
1691 max_functions: 2,
1692 max_total_instructions: 1,
1693 ..AnalysisConfig::default()
1694 },
1695 );
1696
1697 let cfgs = analyzer.analyze_binary(&binary).unwrap();
1698
1699 assert_eq!(cfgs.len(), 1);
1700 assert_eq!(cfgs[0].function.name, "valid");
1701 }
1702
1703 #[test]
1704 fn analyze_binary_rejects_function_count_above_configured_cap() {
1705 let binary = function_analysis_binary(true);
1706 let analyzer = ControlFlowAnalyzer::with_config(
1707 Architecture::X86_64,
1708 AnalysisConfig {
1709 max_functions: 1,
1710 ..AnalysisConfig::default()
1711 },
1712 );
1713
1714 let error = analyzer.analyze_binary(&binary).unwrap_err();
1715
1716 assert!(
1717 matches!(error, BinaryError::ControlFlowError(message) if message.contains("max_functions 1"))
1718 );
1719 }
1720
1721 #[test]
1722 fn analyze_binary_rejects_total_instructions_above_configured_cap() {
1723 let binary = function_analysis_binary(true);
1724 let analyzer = ControlFlowAnalyzer::with_config(
1725 Architecture::X86_64,
1726 AnalysisConfig {
1727 max_total_instructions: 0,
1728 ..AnalysisConfig::default()
1729 },
1730 );
1731
1732 let error = analyzer.analyze_binary(&binary).unwrap_err();
1733
1734 assert!(
1735 matches!(error, BinaryError::ControlFlowError(message) if message.contains("max_total_instructions 0"))
1736 );
1737 }
1738
1739 #[test]
1740 fn analyze_binary_rejects_per_function_instruction_overflow() {
1741 let binary = function_analysis_binary(true);
1742 let analyzer = ControlFlowAnalyzer::with_config(
1743 Architecture::X86_64,
1744 AnalysisConfig {
1745 max_instructions: 0,
1746 ..AnalysisConfig::default()
1747 },
1748 );
1749
1750 let error = analyzer.analyze_binary(&binary).unwrap_err();
1751
1752 assert!(
1753 matches!(error, BinaryError::ControlFlowError(message) if message.contains("max_instructions 0"))
1754 );
1755 }
1756
1757 #[test]
1758 fn analyze_binary_rejects_aggregate_loop_count_above_limit() {
1759 let binary = looping_functions_binary();
1760 let analyzer = ControlFlowAnalyzer::with_config(
1761 Architecture::X86_64,
1762 AnalysisConfig {
1763 max_loops: 1,
1764 ..AnalysisConfig::default()
1765 },
1766 );
1767
1768 let error = analyzer.analyze_binary(&binary).unwrap_err();
1769
1770 assert!(
1771 matches!(error, BinaryError::ControlFlowError(message) if message.contains("max_loops 1"))
1772 );
1773 }
1774
1775 #[test]
1776 fn test_basic_block_creation() {
1777 let instructions = vec![
1778 Instruction {
1779 address: 0x1000,
1780 bytes: vec![0x90],
1781 mnemonic: "nop".to_string(),
1782 operands: String::new(),
1783 category: InstructionCategory::Unknown,
1784 flow: FlowType::Sequential,
1785 size: 1,
1786 },
1787 Instruction {
1788 address: 0x1001,
1789 bytes: vec![0xc3],
1790 mnemonic: "ret".to_string(),
1791 operands: String::new(),
1792 category: InstructionCategory::Control,
1793 flow: FlowType::Return,
1794 size: 1,
1795 },
1796 ];
1797
1798 let analyzer = ControlFlowAnalyzer::new(Architecture::X86_64);
1799 let blocks = analyzer.build_basic_blocks(&instructions).unwrap();
1800
1801 assert_eq!(blocks.len(), 1);
1802 assert_eq!(blocks[0].instructions.len(), 2);
1803 assert_eq!(blocks[0].start_address, 0x1000);
1804 assert_eq!(blocks[0].end_address, 0x1002);
1805 }
1806
1807 fn instruction(address: u64, mnemonic: &str, flow: FlowType) -> Instruction {
1808 Instruction {
1809 address,
1810 bytes: vec![0x90],
1811 mnemonic: mnemonic.to_string(),
1812 operands: String::new(),
1813 category: InstructionCategory::Control,
1814 flow,
1815 size: 1,
1816 }
1817 }
1818
1819 #[test]
1820 fn calls_do_not_split_intra_procedural_basic_blocks() {
1821 let instructions = vec![
1822 instruction(0x1000, "call", FlowType::Call(0x2000)),
1823 instruction(0x1001, "nop", FlowType::Sequential),
1824 instruction(0x1002, "ret", FlowType::Return),
1825 ];
1826
1827 let analyzer = ControlFlowAnalyzer::new(Architecture::X86_64);
1828 let blocks = analyzer.build_basic_blocks(&instructions).unwrap();
1829
1830 assert_eq!(blocks.len(), 1);
1831 assert_eq!(blocks[0].instructions.len(), 3);
1832 }
1833
1834 #[test]
1835 fn coincident_branch_and_fallthrough_edges_are_not_duplicated() {
1836 let instructions = vec![
1837 instruction(0x1000, "je", FlowType::ConditionalJump(0x1001)),
1838 instruction(0x1001, "ret", FlowType::Return),
1839 ];
1840
1841 let analyzer = ControlFlowAnalyzer::new(Architecture::X86_64);
1842 let blocks = analyzer.build_basic_blocks(&instructions).unwrap();
1843
1844 assert_eq!(blocks.len(), 2);
1845 assert_eq!(blocks[0].successors, vec![1]);
1846 assert_eq!(blocks[1].predecessors, vec![0]);
1847 }
1848
1849 #[test]
1850 fn halstead_metrics_remain_finite_without_operands() {
1851 let instructions = vec![
1852 instruction(0x1000, "nop", FlowType::Sequential),
1853 instruction(0x1001, "ret", FlowType::Return),
1854 ];
1855 let mut analyzer = ControlFlowAnalyzer::new(Architecture::X86_64);
1856 let mut blocks = analyzer.build_basic_blocks(&instructions).unwrap();
1857 analyzer.build_dominator_tree(&mut blocks).unwrap();
1858 let dominator_index = ControlFlowAnalyzer::build_dominator_index(&blocks).unwrap();
1859 let metrics = analyzer.calculate_complexity(&blocks, &dominator_index);
1860 let halstead = metrics.halstead_metrics.unwrap();
1861
1862 assert!(halstead.calculated_length.is_finite());
1863 assert!(halstead.volume.is_finite());
1864 }
1865
1866 #[test]
1867 fn public_cfg_algorithms_reject_out_of_range_edges() {
1868 let mut blocks = vec![BasicBlock {
1869 id: 0,
1870 start_address: 0,
1871 end_address: 1,
1872 instructions: vec![instruction(0, "jmp", FlowType::Jump(2))],
1873 successors: vec![1],
1874 predecessors: Vec::new(),
1875 block_type: BlockType::Normal,
1876 dominator: None,
1877 dominance_frontier: Vec::new(),
1878 }];
1879 let mut analyzer = ControlFlowAnalyzer::new(Architecture::X86_64);
1880
1881 assert!(analyzer.build_dominator_tree(&mut blocks).is_err());
1882 assert!(analyzer.analyze_loops(&mut blocks).is_err());
1883 }
1884
1885 fn block(id: usize, successors: Vec<usize>, predecessors: Vec<usize>) -> BasicBlock {
1886 BasicBlock {
1887 id,
1888 start_address: id as u64,
1889 end_address: id as u64 + 1,
1890 instructions: vec![instruction(id as u64, "jmp", FlowType::Jump(0))],
1891 successors,
1892 predecessors,
1893 block_type: BlockType::Normal,
1894 dominator: None,
1895 dominance_frontier: Vec::new(),
1896 }
1897 }
1898
1899 #[test]
1900 fn loop_analysis_merges_latches_and_sets_nesting_level() {
1901 let mut blocks = vec![
1902 block(0, vec![1, 2], vec![1, 2]),
1903 block(1, vec![0], vec![0]),
1904 block(2, vec![0], vec![0]),
1905 ];
1906 let mut analyzer = ControlFlowAnalyzer::new(Architecture::X86_64);
1907
1908 let loops = analyzer.analyze_loops(&mut blocks).unwrap();
1909
1910 assert_eq!(loops.len(), 1);
1911 assert_eq!(loops[0].header_block, 0);
1912 assert_eq!(loops[0].body_blocks, vec![1, 2]);
1913 assert_eq!(loops[0].nesting_level, 1);
1914 assert_eq!(loops[0].loop_type, LoopType::Infinite);
1915 }
1916
1917 #[test]
1918 fn loop_analysis_accepts_exact_aggregate_body_limit() {
1919 let mut blocks = vec![
1920 block(0, vec![1, 2], vec![1, 2]),
1921 block(1, vec![0], vec![0]),
1922 block(2, vec![0], vec![0]),
1923 ];
1924 let mut analyzer = ControlFlowAnalyzer::with_config(
1925 Architecture::X86_64,
1926 AnalysisConfig {
1927 max_loops: 1,
1928 max_total_loop_body_blocks: 2,
1929 ..AnalysisConfig::default()
1930 },
1931 );
1932
1933 let loops = analyzer.analyze_loops(&mut blocks).unwrap();
1934
1935 assert_eq!(loops.len(), 1);
1936 assert_eq!(loops[0].body_blocks, vec![1, 2]);
1937 }
1938
1939 #[test]
1940 fn loop_analysis_rejects_aggregate_body_membership_above_limit() {
1941 let mut blocks = vec![
1942 block(0, vec![1, 2], vec![1, 2]),
1943 block(1, vec![0], vec![0]),
1944 block(2, vec![0], vec![0]),
1945 ];
1946 let mut analyzer = ControlFlowAnalyzer::with_config(
1947 Architecture::X86_64,
1948 AnalysisConfig {
1949 max_total_loop_body_blocks: 1,
1950 ..AnalysisConfig::default()
1951 },
1952 );
1953
1954 let error = analyzer.analyze_loops(&mut blocks).unwrap_err();
1955
1956 assert!(
1957 matches!(error, BinaryError::ControlFlowError(message) if message.contains("max_total_loop_body_blocks 1"))
1958 );
1959 }
1960
1961 #[test]
1962 fn loop_analysis_rejects_loop_count_above_limit() {
1963 let mut blocks = vec![
1964 block(0, vec![1], Vec::new()),
1965 block(1, vec![1, 2], vec![0, 1]),
1966 block(2, vec![2], vec![1, 2]),
1967 ];
1968 let mut analyzer = ControlFlowAnalyzer::with_config(
1969 Architecture::X86_64,
1970 AnalysisConfig {
1971 max_loops: 1,
1972 ..AnalysisConfig::default()
1973 },
1974 );
1975
1976 let error = analyzer.analyze_loops(&mut blocks).unwrap_err();
1977
1978 assert!(
1979 matches!(error, BinaryError::ControlFlowError(message) if message.contains("max_loops 1"))
1980 );
1981 }
1982
1983 #[test]
1984 fn dominator_index_handles_a_long_chain_in_linear_space() {
1985 const BLOCK_COUNT: usize = 20_000;
1986 let mut blocks = Vec::new();
1987 blocks.try_reserve(BLOCK_COUNT).unwrap();
1988 for id in 0..BLOCK_COUNT {
1989 let mut basic_block = block(id, Vec::new(), Vec::new());
1990 basic_block.dominator = Some(id.saturating_sub(1));
1991 if id + 1 < BLOCK_COUNT {
1992 basic_block.successors = vec![id + 1, id + 1];
1994 }
1995 blocks.push(basic_block);
1996 }
1997
1998 let index = ControlFlowAnalyzer::build_dominator_index(&blocks).unwrap();
1999
2000 assert_eq!(index.nesting_depth[0], 0);
2001 assert_eq!(index.nesting_depth[BLOCK_COUNT - 1], 19_999);
2002 assert!(index.dominates(0, BLOCK_COUNT - 1));
2003 assert!(!index.dominates(BLOCK_COUNT - 1, 0));
2004 }
2005
2006 #[test]
2007 fn adding_high_fan_in_edges_keeps_each_predecessor_unique() {
2008 const SOURCE_COUNT: usize = 20_000;
2009 let target = SOURCE_COUNT;
2010 let mut blocks: Vec<_> = (0..=SOURCE_COUNT)
2011 .map(|id| block(id, Vec::new(), Vec::new()))
2012 .collect();
2013
2014 for source in 0..SOURCE_COUNT {
2015 ControlFlowAnalyzer::add_cfg_edge(&mut blocks, source, target);
2016 ControlFlowAnalyzer::add_cfg_edge(&mut blocks, source, target);
2017 }
2018
2019 assert_eq!(blocks[target].predecessors.len(), SOURCE_COUNT);
2020 assert_eq!(blocks[target].predecessors[0], 0);
2021 assert_eq!(
2022 blocks[target].predecessors[SOURCE_COUNT - 1],
2023 SOURCE_COUNT - 1
2024 );
2025 }
2026}