foundry_evm_coverage/
anchors.rs1use super::{CoverageItemKind, ItemAnchor, SourceLocation};
2use crate::analysis::SourceAnalysis;
3use alloy_primitives::map::rustc_hash::FxHashSet;
4use eyre::ensure;
5use foundry_compilers::artifacts::sourcemap::{SourceElement, SourceMap};
6use foundry_evm_core::ic::IcPcMap;
7use revm::bytecode::opcode;
8
9pub fn find_anchors(
11 bytecode: &[u8],
12 source_map: &SourceMap,
13 ic_pc_map: &IcPcMap,
14 analysis: &SourceAnalysis,
15) -> Vec<ItemAnchor> {
16 let mut seen_sources = FxHashSet::default();
17 source_map
18 .iter()
19 .filter_map(|element| element.index())
20 .filter(|&source| seen_sources.insert(source))
21 .flat_map(|source| analysis.items_for_source_enumerated(source))
22 .filter_map(|(item_id, item)| {
23 match item.kind {
24 CoverageItemKind::Branch { path_id, is_first_opcode: false, .. } => {
25 find_anchor_branch(bytecode, source_map, item_id, &item.loc).map(|anchors| {
26 match path_id {
27 0 => anchors.0,
28 1 => anchors.1,
29 _ => panic!("too many path IDs for branch"),
30 }
31 })
32 }
33 _ => find_anchor_simple(source_map, ic_pc_map, item_id, &item.loc),
34 }
35 .inspect_err(|err| warn!(%item, %err, "could not find anchor"))
36 .ok()
37 })
38 .collect()
39}
40
41pub fn find_anchor_simple(
43 source_map: &SourceMap,
44 ic_pc_map: &IcPcMap,
45 item_id: u32,
46 loc: &SourceLocation,
47) -> eyre::Result<ItemAnchor> {
48 let instruction =
49 source_map.iter().position(|element| is_in_source_range(element, loc)).ok_or_else(
50 || eyre::eyre!("Could not find anchor: No matching instruction in range {loc}"),
51 )?;
52
53 Ok(ItemAnchor {
54 instruction: ic_pc_map.get(instruction as u32).ok_or_else(|| {
55 eyre::eyre!("We found an anchor, but we can't translate it to a program counter")
56 })?,
57 item_id,
58 })
59}
60
61pub fn find_anchor_branch(
90 bytecode: &[u8],
91 source_map: &SourceMap,
92 item_id: u32,
93 loc: &SourceLocation,
94) -> eyre::Result<(ItemAnchor, ItemAnchor)> {
95 let mut anchors: Option<(ItemAnchor, ItemAnchor)> = None;
96 let mut pc = 0;
97 let mut cumulative_push_size = 0;
98 while pc < bytecode.len() {
99 let op = bytecode[pc];
100
101 if (opcode::PUSH1..=opcode::PUSH32).contains(&op) {
106 let element = if let Some(element) = source_map.get(pc - cumulative_push_size) {
107 element
108 } else {
109 break;
112 };
113
114 let push_size = (op - opcode::PUSH1 + 1) as usize;
116 pc += push_size;
117 cumulative_push_size += push_size;
118
119 if is_in_source_range(element, loc) && bytecode[pc + 1] == opcode::JUMPI {
122 ensure!(push_size <= 8, "jump destination overflow");
125
126 let push_bytes_start = pc - push_size + 1;
128 let push_bytes = &bytecode[push_bytes_start..push_bytes_start + push_size];
129 let mut pc_bytes = [0u8; 8];
130 pc_bytes[8 - push_size..].copy_from_slice(push_bytes);
131 let pc_jump = u64::from_be_bytes(pc_bytes);
132 let pc_jump = u32::try_from(pc_jump).expect("PC is too big");
133 anchors = Some((
134 ItemAnchor {
135 item_id,
136 instruction: (pc + 2) as u32,
138 },
139 ItemAnchor { item_id, instruction: pc_jump },
140 ));
141 }
142 }
143 pc += 1;
144 }
145
146 anchors.ok_or_else(|| eyre::eyre!("Could not detect branches in source: {}", loc))
147}
148
149fn is_in_source_range(element: &SourceElement, location: &SourceLocation) -> bool {
151 let source_ids_match = element.index_i32() == location.source_id as i32;
153 if !source_ids_match {
154 return false;
155 }
156
157 let is_within_start = element.offset() >= location.bytes.start;
159 if !is_within_start {
160 return false;
161 }
162
163 let start_of_ranges = location.bytes.start.max(element.offset());
164 let end_of_ranges =
165 (location.bytes.start + location.len()).min(element.offset() + element.length());
166 let within_ranges = start_of_ranges <= end_of_ranges;
167 if !within_ranges {
168 return false;
169 }
170
171 true
172}