1use super::{
2 CoverageItemKind, ExecutionAnchor, ExecutionAnchorKind, ItemAnchor, ItemAnchors, SourceLocation,
3};
4use crate::analysis::{EmptySpecialFunctionKind, SourceAnalysis};
5use alloy_primitives::map::rustc_hash::FxHashSet;
6use eyre::ensure;
7use foundry_compilers::artifacts::sourcemap::{SourceElement, SourceMap};
8use foundry_evm_core::{bytecode::InstIter, ic::IcPcMap};
9use revm::bytecode::opcode;
10
11impl ItemAnchor {
12 fn from_instruction(
13 instruction: Option<usize>,
14 ic_pc_map: &IcPcMap,
15 item_id: u32,
16 loc: &SourceLocation,
17 ) -> eyre::Result<Self> {
18 let instruction = instruction.ok_or_else(|| {
19 eyre::eyre!("Could not find anchor: No matching instruction in range {loc}")
20 })?;
21 Ok(Self {
22 instruction: ic_pc_map.get(instruction as u32).ok_or_else(|| {
23 eyre::eyre!("We found an anchor, but we can't translate it to a program counter")
24 })?,
25 item_id,
26 })
27 }
28
29 fn from_branch(item_id: u32, next_pc: usize, immediate: &[u8]) -> eyre::Result<(Self, Self)> {
30 ensure!(immediate.len() <= 4, "jump destination overflow");
32 let mut pc_bytes = [0u8; 4];
33 pc_bytes[4 - immediate.len()..].copy_from_slice(immediate);
34 let pc_jump = u32::from_be_bytes(pc_bytes);
35 Ok((
36 Self { item_id, instruction: (next_pc + 1).try_into()? },
37 Self { item_id, instruction: pc_jump },
38 ))
39 }
40}
41
42pub fn find_anchors(
44 bytecode: &[u8],
45 source_map: &SourceMap,
46 ic_pc_map: &IcPcMap,
47 analysis: &SourceAnalysis,
48) -> ItemAnchors {
49 let mut anchors = ItemAnchors::default();
50 let index = AnchorIndex::new(bytecode, source_map, ic_pc_map);
51 let select_branch = |(fallthrough, taken): (ItemAnchor, ItemAnchor), path_id| match path_id {
52 0 => (fallthrough, None),
53 1 => (taken, Some(fallthrough.instruction - 1)),
54 _ => panic!("too many path IDs for branch"),
55 };
56 let mut seen_sources = FxHashSet::default();
57 source_map
58 .iter()
59 .filter_map(|element| element.index())
60 .filter(|&source| seen_sources.insert(source))
61 .flat_map(|source| analysis.items_for_source_enumerated(source))
62 .filter_map(|(item_id, item)| {
63 if analysis.empty_special_function_kind(item_id).is_some() {
64 return None;
65 }
66 let anchor_loc = item.anchor_loc.as_ref().unwrap_or(&item.loc);
67 match item.kind {
68 CoverageItemKind::Branch { path_id: 1, is_first_opcode: true, .. }
69 if item.anchor_loc.is_some() =>
70 {
71 index.simple(item_id, anchor_loc).map(|anchor| (anchor, None)).or_else(|_| {
72 index
73 .branch(item_id, &item.loc, false)
74 .map(|anchors| select_branch(anchors, 1))
75 })
76 }
77 CoverageItemKind::Branch { branch_id, path_id, is_first_opcode: false } => {
78 let exact = analysis.is_ternary_branch(item.loc.source_id as u32, branch_id);
79 index
80 .branch(item_id, anchor_loc, exact)
81 .map(|anchors| select_branch(anchors, path_id))
82 }
83 _ => index.simple(item_id, anchor_loc).map(|anchor| (anchor, None)),
84 }
85 .inspect_err(|err| warn!(%item, %err, "could not find anchor"))
86 .ok()
87 })
88 .for_each(|(anchor, jump)| {
89 if let Some(jump) = jump {
90 anchors.jumps.insert(anchors.anchors.len(), jump);
91 }
92 anchors.anchors.push(anchor);
93 });
94 anchors
95}
96
97pub fn find_execution_anchors(
100 source_id: u32,
101 contract_name: &str,
102 analysis: &SourceAnalysis,
103) -> Vec<ExecutionAnchor> {
104 analysis
105 .empty_special_function_ids(source_id, contract_name)
106 .filter_map(|item_id| {
107 analysis.empty_special_function_kind(item_id).map(|kind| ExecutionAnchor {
108 item_id,
109 kind: match kind {
110 EmptySpecialFunctionKind::Constructor => ExecutionAnchorKind::Constructor,
111 EmptySpecialFunctionKind::Receive => ExecutionAnchorKind::Receive,
112 EmptySpecialFunctionKind::Fallback => ExecutionAnchorKind::Fallback,
113 },
114 })
115 })
116 .collect()
117}
118
119struct AnchorIndex<'a> {
121 source_map: &'a SourceMap,
122 ic_pc_map: &'a IcPcMap,
123 entries: Vec<(i32, u32, usize)>,
125 branches: Vec<(usize, usize, &'a [u8])>,
127}
128
129impl<'a> AnchorIndex<'a> {
130 fn new(bytecode: &'a [u8], source_map: &'a SourceMap, ic_pc_map: &'a IcPcMap) -> Self {
131 let mut entries = source_map
132 .iter()
133 .enumerate()
134 .map(|(instruction, element)| (element.index_i32(), element.offset(), instruction))
135 .collect::<Vec<_>>();
136 entries.sort_unstable();
137 let mut branches = Vec::new();
138 for (ic, (pc, inst)) in InstIter::new(bytecode).with_pc().enumerate() {
139 if (opcode::PUSH1..=opcode::PUSH32).contains(&inst.opcode.get()) {
140 if source_map.get(ic).is_none() {
142 break;
143 }
144 let next_pc = pc + inst.immediate.len() + 1;
145 if bytecode.get(next_pc).copied() == Some(opcode::JUMPI) {
146 branches.push((ic, next_pc, inst.immediate));
147 }
148 }
149 }
150 Self { source_map, ic_pc_map, entries, branches }
151 }
152
153 fn simple(&self, item_id: u32, loc: &SourceLocation) -> eyre::Result<ItemAnchor> {
155 let source = loc.source_id as i32;
156 let start = self
157 .entries
158 .partition_point(|&(id, offset, _)| (id, offset) < (source, loc.bytes.start));
159 let end = self.entries.partition_point(|&(id, offset, _)| {
160 (id, offset) <= (source, loc.bytes.start + loc.len())
161 });
162 let instruction = self.entries[start..end]
163 .iter()
164 .map(|&(_, _, instruction)| instruction)
165 .filter(|&instruction| is_in_source_range(&self.source_map[instruction], loc))
166 .min();
167 ItemAnchor::from_instruction(instruction, self.ic_pc_map, item_id, loc)
168 }
169
170 fn branch(
174 &self,
175 item_id: u32,
176 loc: &SourceLocation,
177 exact: bool,
178 ) -> eyre::Result<(ItemAnchor, ItemAnchor)> {
179 let mut anchors = None;
180 for &(ic, next_pc, immediate) in &self.branches {
181 let matches = if exact {
182 self.source_map.get(ic + 1).is_some_and(|jump| {
183 jump.index() == Some(loc.source_id as u32)
184 && jump.offset() == loc.bytes.start
185 && jump.length() == loc.len()
186 })
187 } else {
188 is_in_source_range(&self.source_map[ic], loc)
189 };
190 if matches {
191 let found = ItemAnchor::from_branch(item_id, next_pc, immediate)?;
192 if exact {
194 return Ok(found);
195 }
196 anchors = Some(found);
197 }
198 }
199 anchors.ok_or_else(|| eyre::eyre!("Could not detect branches in source: {}", loc))
200 }
201}
202
203fn is_in_source_range(element: &SourceElement, location: &SourceLocation) -> bool {
205 let source_ids_match = element.index_i32() == location.source_id as i32;
207 if !source_ids_match {
208 return false;
209 }
210
211 let is_within_start = element.offset() >= location.bytes.start;
213 if !is_within_start {
214 return false;
215 }
216
217 let start_of_ranges = location.bytes.start.max(element.offset());
218 let end_of_ranges =
219 (location.bytes.start + location.len()).min(element.offset() + element.length());
220 start_of_ranges <= end_of_ranges
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use foundry_compilers::artifacts::sourcemap;
227
228 #[test]
229 fn ternary_anchor_rejects_missing_node_mapping() {
230 let loc =
231 SourceLocation { source_id: 0, contract_name: "T".into(), bytes: 10..30, lines: 1..2 };
232 let bytecode = [opcode::PUSH1, 6, opcode::JUMPI, opcode::PUSH1, 7, opcode::JUMPI];
233 let inner_only = sourcemap::parse("15:5:0;;;").unwrap();
235 let ic_pc_map = IcPcMap::new(&bytecode);
236 let index = AnchorIndex::new(&bytecode, &inner_only, &ic_pc_map);
237 assert!(index.branch(0, &loc, true).is_err());
238 }
239}