1use crate::executors::{
16 Executor,
17 corpus::{DynamicTargetCtx, WorkerCorpus, register_replay_created, rollback_replay_created},
18 corpus_io::{canonical_replay_dirs, read_corpus_dir},
19 invariant::execute_tx,
20};
21use alloy_json_abi::Function;
22use alloy_primitives::{Address, B256, hex};
23use eyre::{Result, eyre};
24use foundry_evm_core::evm::FoundryEvmNetwork;
25use foundry_evm_coverage::HitMaps;
26use foundry_evm_fuzz::invariant::FuzzRunIdentifiedContracts;
27use std::{
28 collections::{BTreeMap, HashSet},
29 fmt,
30 fs::File,
31 io::{BufWriter, Write},
32 path::{Path, PathBuf},
33};
34use uuid::Uuid;
35
36#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
38pub enum ShowmapDomain {
39 #[default]
40 Evm,
41 Sancov,
42 Both,
43}
44
45impl ShowmapDomain {
46 pub const fn includes_evm(self) -> bool {
47 matches!(self, Self::Evm | Self::Both)
48 }
49 pub const fn includes_sancov(self) -> bool {
50 matches!(self, Self::Sancov | Self::Both)
51 }
52}
53
54impl fmt::Display for ShowmapDomain {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 match self {
57 Self::Evm => f.write_str("evm"),
58 Self::Sancov => f.write_str("sancov"),
59 Self::Both => f.write_str("both"),
60 }
61 }
62}
63
64#[derive(Clone, Debug)]
66pub struct ShowmapOpts {
67 pub out_dir: PathBuf,
69 pub approach: String,
72 pub trial: String,
74 pub per_input: bool,
76 pub domain: ShowmapDomain,
78}
79
80#[derive(Clone, Debug, Default)]
82pub struct ShowmapStats {
83 pub corpus_entries: usize,
85 pub showmap_files: usize,
87 pub skipped_entries: usize,
90 pub sancov_requested: bool,
93 pub sancov_observed: bool,
95}
96
97pub fn replay_corpus_to_showmap<FEN: FoundryEvmNetwork>(
104 executor: &Executor<FEN>,
105 corpus_dir: &Path,
106 fuzzed_function: Option<&Function>,
107 fuzzed_contracts: Option<&FuzzRunIdentifiedContracts>,
108 dynamic: Option<&DynamicTargetCtx<'_>>,
109 opts: &ShowmapOpts,
110) -> Result<ShowmapStats> {
111 let replay_dirs = canonical_replay_dirs(corpus_dir);
112 if !replay_dirs.iter().any(|d| d.is_dir()) {
113 return Err(eyre!("corpus directory not found: {}", corpus_dir.display()));
114 }
115
116 let approach_dir = opts.out_dir.join(&opts.approach);
117 foundry_common::fs::create_dir_all(&approach_dir)?;
118
119 let mut stats =
120 ShowmapStats { sancov_requested: opts.domain.includes_sancov(), ..Default::default() };
121 let mut evm_buf: BTreeMap<(B256, u32), u64> = BTreeMap::new();
124 let mut san_buf: Vec<u64> = Vec::new();
125
126 let mut seen_uuids: HashSet<Uuid> = HashSet::new();
128 let entries =
129 replay_dirs.iter().flat_map(|d| read_corpus_dir(d)).filter(|e| seen_uuids.insert(e.uuid));
130
131 for entry in entries {
132 let tx_seq = match entry.read_tx_seq() {
133 Ok(seq) if !seq.is_empty() => seq,
134 Ok(_) => continue,
135 Err(err) => {
136 debug!(target: "showmap", %err, ?entry.path, "failed to read corpus entry");
137 continue;
138 }
139 };
140
141 let mut had_replayable = false;
142 let mut executor = executor.clone();
143 let mut created: Vec<Address> = Vec::new();
145 for tx in &tx_seq {
146 if !WorkerCorpus::can_replay_tx(tx, fuzzed_function, fuzzed_contracts) {
147 continue;
148 }
149 had_replayable = true;
150
151 let mut call_result = execute_tx(&mut executor, tx)?;
152 if opts.domain.includes_evm() {
159 accumulate_evm(&mut evm_buf, call_result.line_coverage.as_ref());
160 }
161 if opts.domain.includes_sancov() {
162 accumulate_sancov(&mut san_buf, call_result.sancov_coverage.as_deref());
163 if let Some(buf) = call_result.sancov_coverage.as_mut() {
164 buf.fill(0);
165 }
166 }
167
168 register_replay_created(
169 &call_result.state_changeset,
170 dynamic,
171 fuzzed_contracts,
172 &mut created,
173 );
174
175 if fuzzed_contracts.is_some() {
177 executor.commit(&mut call_result);
178 }
179 }
180 rollback_replay_created(fuzzed_contracts, created);
181
182 if !had_replayable {
183 stats.skipped_entries += 1;
184 continue;
185 }
186 stats.corpus_entries += 1;
187 if !stats.sancov_observed && san_buf.iter().any(|&x| x != 0) {
188 stats.sancov_observed = true;
189 }
190
191 if opts.per_input {
192 let stem = format!("{}__{}-{}", opts.trial, entry.uuid, entry.timestamp);
194 stats.showmap_files +=
195 write_showmap_file(&approach_dir.join(format!("{stem}.txt")), &evm_buf, &san_buf)?;
196 evm_buf.clear();
198 san_buf.fill(0);
199 }
200 }
201
202 if !opts.per_input {
203 stats.showmap_files += write_showmap_file(
205 &approach_dir.join(format!("{}.txt", opts.trial)),
206 &evm_buf,
207 &san_buf,
208 )?;
209 }
210
211 Ok(stats)
212}
213
214fn accumulate_evm(dst: &mut BTreeMap<(B256, u32), u64>, src: Option<&HitMaps>) {
216 let Some(maps) = src else { return };
217 for (hash, hitmap) in maps.iter() {
218 for (pc, hits) in hitmap.iter() {
219 let slot = dst.entry((*hash, pc)).or_default();
220 *slot = slot.saturating_add(hits as u64);
221 }
222 }
223}
224
225fn accumulate_sancov(dst: &mut Vec<u64>, src: Option<&[u8]>) {
227 let Some(src) = src else { return };
228 if dst.len() < src.len() {
229 dst.resize(src.len(), 0);
230 }
231 for (d, &s) in dst.iter_mut().zip(src) {
232 if s != 0 {
233 *d = d.saturating_add(s as u64);
234 }
235 }
236}
237
238fn write_showmap_file(path: &Path, evm: &BTreeMap<(B256, u32), u64>, san: &[u64]) -> Result<usize> {
241 let has_evm = evm.values().any(|&c| c != 0);
243 let has_san = san.iter().any(|&c| c != 0);
244 if !has_evm && !has_san {
245 return Ok(0);
246 }
247 let mut w = BufWriter::new(File::create(path)?);
248 write_evm(&mut w, evm)?;
249 write_sancov(&mut w, san)?;
250 w.flush()?;
251 Ok(1)
252}
253
254fn write_evm<W: Write>(out: &mut W, evm: &BTreeMap<(B256, u32), u64>) -> std::io::Result<()> {
258 for ((hash, pc), count) in evm {
259 if *count == 0 {
260 continue;
261 }
262 let h = hex::encode(&hash.as_slice()[..8]);
263 writeln!(out, "evm_{h}_{pc:04x}:{count}")?;
264 }
265 Ok(())
266}
267
268fn write_sancov<W: Write>(out: &mut W, bitmap: &[u64]) -> std::io::Result<()> {
269 for (idx, &count) in bitmap.iter().enumerate() {
270 if count != 0 {
271 writeln!(out, "sancov_0x{idx:04x}:{count}")?;
274 }
275 }
276 Ok(())
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 fn temp_dir() -> PathBuf {
284 let dir = std::env::temp_dir().join(format!("foundry-showmap-{}", Uuid::new_v4()));
285 std::fs::create_dir_all(&dir).unwrap();
286 dir
287 }
288
289 #[test]
290 fn accumulate_sancov_resizes_and_saturating_adds() {
291 let mut dst: Vec<u64> = vec![10];
292 accumulate_sancov(&mut dst, Some(&[1u8, 2, 3]));
293 assert_eq!(dst, vec![11, 2, 3]);
294 }
295
296 #[test]
297 fn write_evm_emits_only_nonzero_deterministic_ids() {
298 let mut buf: Vec<u8> = Vec::new();
299 let h = B256::with_last_byte(0xab);
300 let mut evm = BTreeMap::new();
301 evm.insert((h, 1u32), 0u64); evm.insert((h, 0x2au32), 3u64);
303 write_evm(&mut buf, &evm).unwrap();
304 let h_hex = hex::encode(&h.as_slice()[..8]);
305 assert_eq!(String::from_utf8(buf).unwrap(), format!("evm_{h_hex}_002a:3\n"));
306 }
307
308 #[test]
309 fn write_sancov_emits_only_nonzero_hex_ids() {
310 let mut buf: Vec<u8> = Vec::new();
311 write_sancov(&mut buf, &[0, 3, 0, 1]).unwrap();
312 assert_eq!(String::from_utf8(buf).unwrap(), "sancov_0x0001:3\nsancov_0x0003:1\n");
313 }
314
315 #[test]
316 fn write_showmap_file_skips_when_empty() {
317 let dir = temp_dir();
318 let path = dir.join("trial.txt");
319 let written = write_showmap_file(&path, &BTreeMap::new(), &[]).unwrap();
320 assert_eq!(written, 0);
321 assert!(!path.exists());
322 }
323
324 #[test]
325 fn write_showmap_file_writes_combined_domains() {
326 let dir = temp_dir();
327 let path = dir.join("trial.txt");
328 let h = B256::with_last_byte(0xff);
329 let mut evm = BTreeMap::new();
330 evm.insert((h, 7u32), 5u64);
331 let written = write_showmap_file(&path, &evm, &[2]).unwrap();
332 assert_eq!(written, 1);
333 let body = std::fs::read_to_string(&path).unwrap();
334 let h_hex = hex::encode(&h.as_slice()[..8]);
335 assert_eq!(body, format!("evm_{h_hex}_0007:5\nsancov_0x0000:2\n"));
336 }
337
338 #[test]
339 fn canonical_replay_dirs_collects_all_workers() {
340 let dir = temp_dir();
341 let w0 = dir.join("worker0").join("corpus");
342 let w1 = dir.join("worker1").join("corpus");
343 std::fs::create_dir_all(&w0).unwrap();
344 std::fs::create_dir_all(&w1).unwrap();
345 assert_eq!(canonical_replay_dirs(&dir), vec![w0, w1]);
346 }
347
348 #[test]
349 fn canonical_replay_dirs_falls_back_when_no_workers() {
350 let dir = temp_dir();
351 assert_eq!(canonical_replay_dirs(&dir), vec![dir]);
352 }
353}