Skip to main content

foundry_cheatcodes/
fs.rs

1//! Implementations of [`Filesystem`](spec::Group::Filesystem) cheatcodes.
2
3use super::string::parse;
4use crate::{
5    Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result, Vm::*, inspector::exec_create,
6};
7use alloy_dyn_abi::DynSolType;
8use alloy_json_abi::ContractObject;
9use alloy_network::{Network, ReceiptResponse};
10use alloy_primitives::{Bytes, FixedBytes, U256, hex, map::Entry};
11use alloy_sol_types::SolValue;
12use dialoguer::{Input, Password};
13use forge_script_sequence::{BroadcastReader, TransactionWithMetadata};
14use foundry_common::{contracts::ContractData, fs};
15use foundry_config::fs_permissions::FsAccessKind;
16use foundry_evm_core::{FoundryTransaction, env::FoundryContextExt, evm::FoundryEvmNetwork};
17use revm::{
18    context::{Cfg, ContextTr, CreateScheme, JournalTr},
19    interpreter::CreateInputs,
20};
21use revm_inspectors::tracing::types::CallKind;
22use semver::Version;
23use std::{
24    io::{BufRead, BufReader},
25    path::{Path, PathBuf},
26    process::Command,
27    sync::mpsc,
28    thread,
29    time::{SystemTime, UNIX_EPOCH},
30};
31use walkdir::WalkDir;
32
33/// Parsed artifact path components.
34#[derive(Debug, Default, PartialEq, Eq)]
35struct ParsedArtifactPath<'a> {
36    file: Option<PathBuf>,
37    contract_name: Option<&'a str>,
38    version: Option<Version>,
39    profile: Option<&'a str>,
40}
41
42/// Parses an artifact path string into its components.
43///
44/// Supports the following formats:
45/// - `path/to/contract.sol`
46/// - `path/to/contract.sol:ContractName`
47/// - `path/to/contract.sol:ContractName:0.8.23`
48/// - `path/to/contract.sol:ContractName:profile`
49/// - `path/to/contract.sol:0.8.23`
50/// - `path/to/contract.sol:profile`
51/// - `ContractName`
52/// - `ContractName:0.8.23`
53/// - `ContractName:profile`
54fn parse_artifact_path(path: &str) -> std::result::Result<ParsedArtifactPath<'_>, String> {
55    // A Windows drive separator belongs to the file, not the artifact's suffix fields.
56    // Recognize it on every host so parsing does not depend on the current platform.
57    let unprefixed = path.strip_prefix(r"\\?\").unwrap_or(path);
58    let drive_prefix_len = match unprefixed.as_bytes() {
59        [drive, b':', b'/' | b'\\', ..] if drive.is_ascii_alphabetic() => {
60            path.len() - unprefixed.len() + 2
61        }
62        _ => 0,
63    };
64    let mut parts = path[drive_prefix_len..].split(':');
65
66    let mut file = None;
67    let mut contract_name = None;
68    let mut version = None;
69    let mut profile = None;
70
71    let path_or_name = parts.next().unwrap();
72    let path_or_name = &path[..drive_prefix_len + path_or_name.len()];
73    if path_or_name.contains('.') {
74        file = Some(PathBuf::from(path_or_name));
75        if let Some(name_or_version_or_profile) = parts.next() {
76            if name_or_version_or_profile.contains('.')
77                || Version::parse(name_or_version_or_profile).is_ok()
78            {
79                version = Some(name_or_version_or_profile);
80            } else {
81                contract_name = Some(name_or_version_or_profile);
82                if let Some(version_or_profile) = parts.next() {
83                    if version_or_profile.contains('.')
84                        || Version::parse(version_or_profile).is_ok()
85                    {
86                        version = Some(version_or_profile);
87                    } else {
88                        profile = Some(version_or_profile);
89                    }
90                }
91            }
92        }
93    } else {
94        contract_name = Some(path_or_name);
95        if let Some(version_or_profile) = parts.next() {
96            if version_or_profile.contains('.') || Version::parse(version_or_profile).is_ok() {
97                version = Some(version_or_profile);
98            } else {
99                profile = Some(version_or_profile);
100            }
101        }
102    }
103
104    let version = if let Some(version) = version {
105        Some(Version::parse(version).map_err(|e| format!("failed parsing version: {e}"))?)
106    } else {
107        None
108    };
109
110    Ok(ParsedArtifactPath { file, contract_name, version, profile })
111}
112
113impl Cheatcode for existsCall {
114    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
115        let Self { path } = self;
116        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
117        Ok(path.exists().abi_encode())
118    }
119}
120
121impl Cheatcode for fsMetadataCall {
122    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
123        let Self { path } = self;
124        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
125
126        let metadata = path.metadata()?;
127
128        // These fields not available on all platforms; default to 0
129        let [modified, accessed, created] =
130            [metadata.modified(), metadata.accessed(), metadata.created()].map(|time| {
131                time.unwrap_or(UNIX_EPOCH).duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()
132            });
133
134        Ok(FsMetadata {
135            isDir: metadata.is_dir(),
136            isSymlink: metadata.is_symlink(),
137            length: U256::from(metadata.len()),
138            readOnly: metadata.permissions().readonly(),
139            modified: U256::from(modified),
140            accessed: U256::from(accessed),
141            created: U256::from(created),
142        }
143        .abi_encode())
144    }
145}
146
147impl Cheatcode for isDirCall {
148    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
149        let Self { path } = self;
150        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
151        Ok(path.is_dir().abi_encode())
152    }
153}
154
155impl Cheatcode for isFileCall {
156    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
157        let Self { path } = self;
158        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
159        Ok(path.is_file().abi_encode())
160    }
161}
162
163impl Cheatcode for projectRootCall {
164    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
165        let Self {} = self;
166        Ok(state.config.root.display().to_string().abi_encode())
167    }
168}
169
170impl Cheatcode for currentFilePathCall {
171    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
172        let Self {} = self;
173        let artifact = state
174            .config
175            .running_artifact
176            .as_ref()
177            .ok_or_else(|| fmt_err!("no running contract found"))?;
178        let relative = artifact.source.strip_prefix(&state.config.root).unwrap_or(&artifact.source);
179        Ok(relative.display().to_string().abi_encode())
180    }
181}
182
183impl Cheatcode for unixTimeCall {
184    fn apply<FEN: FoundryEvmNetwork>(&self, _state: &mut Cheatcodes<FEN>) -> Result {
185        let Self {} = self;
186        let difference = SystemTime::now()
187            .duration_since(UNIX_EPOCH)
188            .map_err(|e| fmt_err!("failed getting Unix timestamp: {e}"))?;
189        Ok(difference.as_millis().abi_encode())
190    }
191}
192
193impl Cheatcode for closeFileCall {
194    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
195        let Self { path } = self;
196        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
197
198        state.test_context.opened_read_files.remove(&path);
199
200        Ok(Default::default())
201    }
202}
203
204impl Cheatcode for copyFileCall {
205    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
206        let Self { from, to } = self;
207        let from = state.config.ensure_path_allowed(from, FsAccessKind::Read)?;
208        let to = state.config.ensure_path_allowed(to, FsAccessKind::Write)?;
209        state.config.ensure_not_foundry_toml(&to)?;
210
211        let n = fs::copy(from, to)?;
212        Ok(n.abi_encode())
213    }
214}
215
216impl Cheatcode for createDirCall {
217    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
218        let Self { path, recursive } = self;
219        let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
220        if *recursive { fs::create_dir_all(path) } else { fs::create_dir(path) }?;
221        Ok(Default::default())
222    }
223}
224
225impl Cheatcode for readDir_0Call {
226    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
227        let Self { path } = self;
228        read_dir(state, path.as_ref(), 1, false)
229    }
230}
231
232impl Cheatcode for readDir_1Call {
233    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
234        let Self { path, maxDepth } = self;
235        read_dir(state, path.as_ref(), *maxDepth, false)
236    }
237}
238
239impl Cheatcode for readDir_2Call {
240    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
241        let Self { path, maxDepth, followLinks } = self;
242        read_dir(state, path.as_ref(), *maxDepth, *followLinks)
243    }
244}
245
246impl Cheatcode for readFileCall {
247    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
248        let Self { path } = self;
249        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
250        Ok(fs::locked_read_to_string(path)?.abi_encode())
251    }
252}
253
254impl Cheatcode for readFileBinaryCall {
255    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
256        let Self { path } = self;
257        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
258        Ok(fs::locked_read(path)?.abi_encode())
259    }
260}
261
262impl Cheatcode for readLineCall {
263    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
264        let Self { path } = self;
265        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
266
267        // Get reader for previously opened file to continue reading OR initialize new reader
268        let reader = match state.test_context.opened_read_files.entry(path.clone()) {
269            Entry::Occupied(entry) => entry.into_mut(),
270            Entry::Vacant(entry) => entry.insert(BufReader::new(fs::open(path)?)),
271        };
272
273        let mut line: String = String::new();
274        reader.read_line(&mut line)?;
275
276        // Remove trailing newline character, preserving others for cases where it may be important
277        if line.ends_with('\n') {
278            line.pop();
279            if line.ends_with('\r') {
280                line.pop();
281            }
282        }
283
284        Ok(line.abi_encode())
285    }
286}
287
288impl Cheatcode for readLinkCall {
289    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
290        let Self { linkPath: path } = self;
291        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
292        let target = fs::read_link(path)?;
293        Ok(target.display().to_string().abi_encode())
294    }
295}
296
297impl Cheatcode for removeDirCall {
298    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
299        let Self { path, recursive } = self;
300        let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
301        state.config.ensure_not_foundry_toml(&path)?;
302        if *recursive { fs::remove_dir_all(path) } else { fs::remove_dir(path) }?;
303        Ok(Default::default())
304    }
305}
306
307impl Cheatcode for removeFileCall {
308    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
309        let Self { path } = self;
310        let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
311        state.config.ensure_not_foundry_toml(&path)?;
312
313        // also remove from the set if opened previously
314        state.test_context.opened_read_files.remove(&path);
315
316        if state.fs_commit {
317            fs::remove_file(&path)?;
318        }
319
320        Ok(Default::default())
321    }
322}
323
324impl Cheatcode for writeFileCall {
325    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
326        let Self { path, data } = self;
327        write_file(state, path.as_ref(), data.as_bytes())
328    }
329}
330
331impl Cheatcode for writeFileBinaryCall {
332    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
333        let Self { path, data } = self;
334        write_file(state, path.as_ref(), data)
335    }
336}
337
338impl Cheatcode for writeLineCall {
339    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
340        let Self { path, data: line } = self;
341        let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
342        state.config.ensure_not_foundry_toml(&path)?;
343
344        if state.fs_commit {
345            fs::locked_write_line(path, line)?;
346        }
347
348        Ok(Default::default())
349    }
350}
351
352impl Cheatcode for getArtifactPathByCodeCall {
353    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
354        let Self { code } = self;
355        let (artifact_id, _) = state
356            .config
357            .available_artifacts
358            .as_ref()
359            .and_then(|artifacts| artifacts.find_by_creation_code(code))
360            .ok_or_else(|| fmt_err!("no matching artifact found"))?;
361
362        Ok(artifact_id.path.to_string_lossy().abi_encode())
363    }
364}
365
366impl Cheatcode for getArtifactPathByDeployedCodeCall {
367    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
368        let Self { deployedCode } = self;
369        let (artifact_id, _) = state
370            .config
371            .available_artifacts
372            .as_ref()
373            .and_then(|artifacts| artifacts.find_by_deployed_code(deployedCode))
374            .ok_or_else(|| fmt_err!("no matching artifact found"))?;
375
376        Ok(artifact_id.path.to_string_lossy().abi_encode())
377    }
378}
379
380impl Cheatcode for getCodeCall {
381    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
382        let Self { artifactPath: path } = self;
383        Ok(get_artifact_code(state, path, false)?.abi_encode())
384    }
385}
386
387impl Cheatcode for getDeployedCodeCall {
388    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
389        let Self { artifactPath: path } = self;
390        Ok(get_artifact_code(state, path, true)?.abi_encode())
391    }
392}
393
394impl Cheatcode for getSelectorsCall {
395    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
396        let Self { artifactPath: path } = self;
397        let selectors: Vec<FixedBytes<4>> = match get_artifact_source(state, path)? {
398            ArtifactSource::InMemory(data) => data.abi.functions().map(|f| f.selector()).collect(),
399            ArtifactSource::Disk(path) => {
400                let data = read_artifact_file(state, &path)?;
401                // Parse as raw JSON rather than `ContractObject` so we can still read selectors
402                // from artifacts with unlinked bytecode (which `ContractObject` rejects).
403                let json: serde_json::Value = serde_json::from_str(&data)?;
404                let abi =
405                    json.get("abi").ok_or_else(|| fmt_err!("no `abi` field in artifact JSON"))?;
406                let abi: alloy_json_abi::JsonAbi =
407                    serde_json::from_value(abi.clone()).map_err(|e| fmt_err!("{e}"))?;
408                abi.functions().map(|f| f.selector()).collect()
409            }
410        };
411        Ok(selectors.abi_encode())
412    }
413}
414
415impl Cheatcode for deployCode_0Call {
416    fn apply_full<FEN: FoundryEvmNetwork>(
417        &self,
418        ccx: &mut CheatsCtxt<'_, '_, FEN>,
419        executor: &mut dyn CheatcodesExecutor<FEN>,
420    ) -> Result {
421        let Self { artifactPath: path } = self;
422        deploy_code(ccx, executor, path, None, None, None)
423    }
424}
425
426impl Cheatcode for deployCode_1Call {
427    fn apply_full<FEN: FoundryEvmNetwork>(
428        &self,
429        ccx: &mut CheatsCtxt<'_, '_, FEN>,
430        executor: &mut dyn CheatcodesExecutor<FEN>,
431    ) -> Result {
432        let Self { artifactPath: path, constructorArgs: args } = self;
433        deploy_code(ccx, executor, path, Some(args), None, None)
434    }
435}
436
437impl Cheatcode for deployCode_2Call {
438    fn apply_full<FEN: FoundryEvmNetwork>(
439        &self,
440        ccx: &mut CheatsCtxt<'_, '_, FEN>,
441        executor: &mut dyn CheatcodesExecutor<FEN>,
442    ) -> Result {
443        let Self { artifactPath: path, value } = self;
444        deploy_code(ccx, executor, path, None, Some(*value), None)
445    }
446}
447
448impl Cheatcode for deployCode_3Call {
449    fn apply_full<FEN: FoundryEvmNetwork>(
450        &self,
451        ccx: &mut CheatsCtxt<'_, '_, FEN>,
452        executor: &mut dyn CheatcodesExecutor<FEN>,
453    ) -> Result {
454        let Self { artifactPath: path, constructorArgs: args, value } = self;
455        deploy_code(ccx, executor, path, Some(args), Some(*value), None)
456    }
457}
458
459impl Cheatcode for deployCode_4Call {
460    fn apply_full<FEN: FoundryEvmNetwork>(
461        &self,
462        ccx: &mut CheatsCtxt<'_, '_, FEN>,
463        executor: &mut dyn CheatcodesExecutor<FEN>,
464    ) -> Result {
465        let Self { artifactPath: path, salt } = self;
466        deploy_code(ccx, executor, path, None, None, Some((*salt).into()))
467    }
468}
469
470impl Cheatcode for deployCode_5Call {
471    fn apply_full<FEN: FoundryEvmNetwork>(
472        &self,
473        ccx: &mut CheatsCtxt<'_, '_, FEN>,
474        executor: &mut dyn CheatcodesExecutor<FEN>,
475    ) -> Result {
476        let Self { artifactPath: path, constructorArgs: args, salt } = self;
477        deploy_code(ccx, executor, path, Some(args), None, Some((*salt).into()))
478    }
479}
480
481impl Cheatcode for deployCode_6Call {
482    fn apply_full<FEN: FoundryEvmNetwork>(
483        &self,
484        ccx: &mut CheatsCtxt<'_, '_, FEN>,
485        executor: &mut dyn CheatcodesExecutor<FEN>,
486    ) -> Result {
487        let Self { artifactPath: path, value, salt } = self;
488        deploy_code(ccx, executor, path, None, Some(*value), Some((*salt).into()))
489    }
490}
491
492impl Cheatcode for deployCode_7Call {
493    fn apply_full<FEN: FoundryEvmNetwork>(
494        &self,
495        ccx: &mut CheatsCtxt<'_, '_, FEN>,
496        executor: &mut dyn CheatcodesExecutor<FEN>,
497    ) -> Result {
498        let Self { artifactPath: path, constructorArgs: args, value, salt } = self;
499        deploy_code(ccx, executor, path, Some(args), Some(*value), Some((*salt).into()))
500    }
501}
502
503/// Helper function to deploy contract from artifact code.
504/// Uses CREATE2 scheme if salt specified.
505fn deploy_code<FEN: FoundryEvmNetwork>(
506    ccx: &mut CheatsCtxt<'_, '_, FEN>,
507    executor: &mut dyn CheatcodesExecutor<FEN>,
508    path: &str,
509    constructor_args: Option<&Bytes>,
510    value: Option<U256>,
511    salt: Option<U256>,
512) -> Result {
513    let mut bytecode = get_artifact_code(ccx.state, path, false)?.to_vec();
514
515    // If active broadcast then set flag to deploy from code.
516    if let Some(broadcast) = &mut ccx.state.broadcast {
517        broadcast.deploy_from_code = true;
518    }
519
520    if let Some(args) = constructor_args {
521        bytecode.extend_from_slice(args);
522    }
523
524    let scheme =
525        if let Some(salt) = salt { CreateScheme::Create2 { salt } } else { CreateScheme::Create };
526
527    // The nested EVM executes the synthetic create one level deeper, so apply the prank at the
528    // original depth just as the native create inspector would.
529    let depth = ccx.ecx.journal().depth();
530    let mut caller = ccx.caller;
531    if let Some(prank) = ccx.state.get_prank(depth).copied()
532        && depth >= prank.depth
533        && caller == prank.prank_caller
534    {
535        let prank_applied = if depth == prank.depth {
536            caller = prank.new_caller;
537            true
538        } else {
539            false
540        };
541        let prank_applied = if let Some(new_origin) = prank.new_origin {
542            ccx.ecx.tx_mut().set_caller(new_origin);
543            true
544        } else {
545            prank_applied
546        };
547
548        if prank_applied && let Some(applied_prank) = prank.first_time_applied() {
549            ccx.state.pranks.insert(depth, applied_prank);
550        }
551    }
552
553    let outcome = exec_create(
554        executor,
555        CreateInputs::new(
556            caller,
557            scheme,
558            value.unwrap_or(U256::ZERO),
559            bytecode.into(),
560            ccx.gas_limit,
561            0,
562        ),
563        ccx,
564    );
565
566    // Restore the prank state at the original depth as native create cleanup would.
567    if let Some(prank) = ccx.state.get_prank(depth).copied()
568        && depth == prank.depth
569    {
570        ccx.ecx.tx_mut().set_caller(prank.prank_origin);
571        if prank.single_call {
572            std::mem::take(&mut ccx.state.pranks);
573        }
574    }
575
576    let outcome = outcome?;
577
578    if !outcome.result.result.is_ok() {
579        return Err(crate::Error::from(outcome.result.output));
580    }
581
582    let address = outcome.address.ok_or_else(|| fmt_err!("contract creation failed"))?;
583
584    Ok(address.abi_encode())
585}
586
587/// Resolved location of an artifact referenced by a cheatcode path argument.
588enum ArtifactSource<'a> {
589    /// The artifact was matched in the in-memory `available_artifacts` list.
590    InMemory(&'a ContractData),
591    /// The artifact must be read from the given path on disk.
592    Disk(PathBuf),
593}
594
595/// Resolves a cheatcode artifact reference to its source.
596///
597/// Can parse the following input formats:
598/// - `path/to/artifact.json`
599/// - `path/to/contract.sol`
600/// - `path/to/contract.sol:ContractName`
601/// - `path/to/contract.sol:ContractName:0.8.23`
602/// - `path/to/contract.sol:ContractName:profile`
603/// - `path/to/contract.sol:0.8.23`
604/// - `path/to/contract.sol:profile`
605/// - `ContractName`
606/// - `ContractName:0.8.23`
607/// - `ContractName:profile`
608fn get_artifact_source<'a, FEN: FoundryEvmNetwork>(
609    state: &'a Cheatcodes<FEN>,
610    path: &str,
611) -> Result<ArtifactSource<'a>> {
612    if path.ends_with(".json") {
613        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
614        return Ok(ArtifactSource::Disk(path));
615    }
616
617    let parsed =
618        parse_artifact_path(path).map_err(|e| fmt_err!("failed to parse artifact path: {e}"))?;
619    let ParsedArtifactPath { file, contract_name, version, profile } = parsed;
620    let file = file.map(|file| {
621        let cwd = state
622            .config
623            .running_artifact
624            .as_ref()
625            .and_then(|artifact| artifact.source.parent())
626            .unwrap_or(&state.config.paths.root);
627        let relative_cwd = cwd.strip_prefix(&state.config.paths.root).unwrap_or(cwd);
628        let has_matching_remapping = state.config.paths.remappings.iter().any(|remapping| {
629            remapping.context.as_ref().is_none_or(|context| relative_cwd.starts_with(context))
630                && file.strip_prefix(&remapping.name).is_ok()
631        });
632
633        if has_matching_remapping {
634            state.config.paths.resolve_library_import(cwd, &file).map_or(file, |resolved| {
635                resolved.strip_prefix(&state.config.paths.root).unwrap_or(&resolved).to_path_buf()
636            })
637        } else {
638            file
639        }
640    });
641
642    // Use the artifact lookup if present.
643    if let Some(artifacts) =
644        state.config.available_artifacts.as_ref().or(state.config.artifact_lookup.as_ref())
645    {
646        let ambiguous_file_profile =
647            file.is_some() && version.is_none() && profile.is_none() && contract_name.is_some();
648        let filter_artifacts = |treat_ambiguous_as_profile: bool| -> Vec<_> {
649            artifacts
650                .iter()
651                .filter(|(id, _)| {
652                    // name might be in the form of "Counter.0.8.23"
653                    let id_name = id.name.split('.').next().unwrap();
654
655                    if let Some(path) = &file
656                        && !id.source.ends_with(path)
657                    {
658                        return false;
659                    }
660                    if let Some(ref version) = version
661                        && (id.version.minor != version.minor
662                            || id.version.major != version.major
663                            || id.version.patch != version.patch)
664                    {
665                        return false;
666                    }
667                    if let Some(profile) = profile
668                        && id.profile != profile
669                    {
670                        return false;
671                    }
672                    if let Some(name) = contract_name {
673                        if treat_ambiguous_as_profile && ambiguous_file_profile {
674                            return id.profile == name;
675                        }
676
677                        return id_name == name;
678                    }
679
680                    true
681                })
682                .collect()
683        };
684
685        let mut filtered = filter_artifacts(false);
686        if filtered.is_empty() && ambiguous_file_profile {
687            filtered = filter_artifacts(true);
688        }
689
690        let artifact = match &filtered[..] {
691            [] => None,
692            [artifact] => Some(Ok(*artifact)),
693            filtered => {
694                let mut filtered = filtered.to_vec();
695                // If we know the current script/test contract solc version, try to filter by it
696                Some(
697                    state
698                        .config
699                        .running_artifact
700                        .as_ref()
701                        .and_then(|running| {
702                            // Only filter by running version if user did NOT specify a version
703                            if version.is_none() {
704                                filtered.retain(|(id, _)| id.version == running.version);
705
706                                // Return artifact if only one matched
707                                if filtered.len() == 1 {
708                                    return Some(filtered[0]);
709                                }
710                            }
711
712                            // Only filter by running profile if user did NOT specify a profile
713                            if profile.is_none() {
714                                filtered.retain(|(id, _)| id.profile == running.profile);
715
716                                return (filtered.len() == 1).then(|| filtered[0]);
717                            }
718
719                            None
720                        })
721                        .ok_or_else(|| fmt_err!("multiple matching artifacts found")),
722                )
723            }
724        };
725
726        if let Some(artifact) = artifact {
727            return Ok(ArtifactSource::InMemory(artifact?.1));
728        }
729    }
730
731    // Fallback: construct path manually when no artifacts list or no match found
732    let path_in_artifacts = match (file.map(|f| f.to_string_lossy().to_string()), contract_name) {
733        (Some(file), Some(contract_name)) => PathBuf::from(format!("{file}/{contract_name}.json")),
734        (None, Some(contract_name)) => {
735            PathBuf::from(format!("{contract_name}.sol/{contract_name}.json"))
736        }
737        (Some(file), None) => {
738            let name = file.replace(".sol", "");
739            PathBuf::from(format!("{file}/{name}.json"))
740        }
741        _ => bail!("invalid artifact path"),
742    };
743
744    let path = state.config.paths.artifacts.join(path_in_artifacts);
745    let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
746    Ok(ArtifactSource::Disk(path))
747}
748
749/// Reads an artifact JSON file, mapping I/O errors to a helpful message when the
750/// lookup fell through the in-memory artifacts list.
751fn read_artifact_file<FEN: FoundryEvmNetwork>(
752    state: &Cheatcodes<FEN>,
753    path: &Path,
754) -> Result<String> {
755    fs::read_to_string(path).map_err(|e| {
756        if state.config.available_artifacts.is_some() {
757            fmt_err!("no matching artifact found")
758        } else {
759            e.into()
760        }
761    })
762}
763
764/// Returns the bytecode from a JSON artifact file.
765///
766/// See [`get_artifact_source`] for the supported path formats.
767///
768/// This function is safe to use with contracts that have library dependencies.
769/// `alloy_json_abi::ContractObject` validates bytecode during JSON parsing and will
770/// reject artifacts with unlinked library placeholders.
771fn get_artifact_code<FEN: FoundryEvmNetwork>(
772    state: &Cheatcodes<FEN>,
773    path: &str,
774    deployed: bool,
775) -> Result<Bytes> {
776    let maybe_bytecode = match get_artifact_source(state, path)? {
777        ArtifactSource::InMemory(data) => {
778            if deployed { data.deployed_bytecode() } else { data.bytecode() }.cloned()
779        }
780        ArtifactSource::Disk(path) => {
781            let data = read_artifact_file(state, &path)?;
782            let artifact = serde_json::from_str::<ContractObject>(&data)?;
783            if deployed { artifact.deployed_bytecode } else { artifact.bytecode }
784        }
785    };
786    maybe_bytecode.ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?"))
787}
788
789impl Cheatcode for ffiCall {
790    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
791        let Self { commandInput: input } = self;
792        let stdout = ffi_stdout(state, input)?;
793        Ok(decode_ffi_stdout(&stdout).abi_encode())
794    }
795}
796
797impl Cheatcode for ffiUintCall {
798    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
799        let Self { commandInput: input } = self;
800        parse(&ffi_stdout(state, input)?, &DynSolType::Uint(256))
801    }
802}
803
804impl Cheatcode for ffiStringCall {
805    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
806        let Self { commandInput: input } = self;
807        Ok(ffi_stdout(state, input)?.abi_encode())
808    }
809}
810
811impl Cheatcode for ffiBytesCall {
812    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
813        let Self { commandInput: input } = self;
814        let stdout = ffi_stdout(state, input)?;
815        Ok(hex::decode(&stdout)
816            .map_err(|err| fmt_err!("failed parsing ffi stdout as bytes: {err}"))?
817            .abi_encode())
818    }
819}
820
821impl Cheatcode for tryFfiCall {
822    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
823        let Self { commandInput: input } = self;
824        ffi(state, input).map(|res| res.abi_encode())
825    }
826}
827
828impl Cheatcode for promptCall {
829    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
830        let Self { promptText: text } = self;
831        prompt(state, text, prompt_input).map(|res| res.abi_encode())
832    }
833}
834
835impl Cheatcode for promptSecretCall {
836    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
837        let Self { promptText: text } = self;
838        prompt(state, text, prompt_password).map(|res| res.abi_encode())
839    }
840}
841
842impl Cheatcode for promptSecretUintCall {
843    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
844        let Self { promptText: text } = self;
845        parse(&prompt(state, text, prompt_password)?, &DynSolType::Uint(256))
846    }
847}
848
849impl Cheatcode for promptAddressCall {
850    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
851        let Self { promptText: text } = self;
852        parse(&prompt(state, text, prompt_input)?, &DynSolType::Address)
853    }
854}
855
856impl Cheatcode for promptUintCall {
857    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
858        let Self { promptText: text } = self;
859        parse(&prompt(state, text, prompt_input)?, &DynSolType::Uint(256))
860    }
861}
862
863pub(super) fn write_file<FEN: FoundryEvmNetwork>(
864    state: &Cheatcodes<FEN>,
865    path: &Path,
866    contents: &[u8],
867) -> Result {
868    let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
869    // write access to foundry.toml is not allowed
870    state.config.ensure_not_foundry_toml(&path)?;
871
872    if state.fs_commit {
873        fs::locked_write(path, contents)?;
874    }
875
876    Ok(Default::default())
877}
878
879fn read_dir<FEN: FoundryEvmNetwork>(
880    state: &Cheatcodes<FEN>,
881    path: &Path,
882    max_depth: u64,
883    follow_links: bool,
884) -> Result {
885    let root = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
886    let paths: Vec<DirEntry> = WalkDir::new(root)
887        .min_depth(1)
888        .max_depth(max_depth.try_into().unwrap_or(usize::MAX))
889        .follow_links(follow_links)
890        .contents_first(false)
891        .same_file_system(true)
892        .sort_by_file_name()
893        .into_iter()
894        .map(|entry| match entry {
895            Ok(entry) => DirEntry {
896                errorMessage: String::new(),
897                path: entry.path().display().to_string(),
898                depth: entry.depth() as u64,
899                isDir: entry.file_type().is_dir(),
900                isSymlink: entry.path_is_symlink(),
901            },
902            Err(e) => DirEntry {
903                errorMessage: e.to_string(),
904                path: e.path().map(|p| p.display().to_string()).unwrap_or_default(),
905                depth: e.depth() as u64,
906                isDir: false,
907                isSymlink: false,
908            },
909        })
910        .collect();
911    Ok(paths.abi_encode())
912}
913
914fn ffi<FEN: FoundryEvmNetwork>(state: &Cheatcodes<FEN>, input: &[String]) -> Result<FfiResult> {
915    let output = ffi_command(state, input)?;
916
917    let stdout = String::from_utf8(output.stdout)?;
918    let stdout = stdout.trim();
919    Ok(FfiResult {
920        exitCode: output.status.code().unwrap_or(69),
921        stdout: decode_ffi_stdout(stdout),
922        stderr: output.stderr.into(),
923    })
924}
925
926fn ffi_stdout<FEN: FoundryEvmNetwork>(state: &Cheatcodes<FEN>, input: &[String]) -> Result<String> {
927    let output = ffi_command(state, input)?;
928    let stdout = String::from_utf8(output.stdout)?;
929    if !output.status.success() {
930        return Err(fmt_err!(
931            "ffi command {:?} exited with code {}. stderr: {}",
932            input,
933            output.status.code().unwrap_or(69),
934            String::from_utf8_lossy(&output.stderr)
935        ));
936    }
937    if !output.stderr.is_empty() {
938        let stderr = String::from_utf8_lossy(&output.stderr);
939        warn!(target: "cheatcodes", ?input, ?stderr, "ffi command wrote to stderr");
940    }
941    Ok(stdout.trim().to_string())
942}
943
944fn decode_ffi_stdout(stdout: &str) -> Bytes {
945    hex::decode(stdout).unwrap_or_else(|_| stdout.as_bytes().to_vec()).into()
946}
947
948fn ffi_command<FEN: FoundryEvmNetwork>(
949    state: &Cheatcodes<FEN>,
950    input: &[String],
951) -> Result<std::process::Output> {
952    ensure!(
953        state.config.ffi,
954        "FFI is disabled; add the `--ffi` flag to allow tests to call external commands"
955    );
956    ensure!(!input.is_empty() && !input[0].is_empty(), "can't execute empty command");
957    let mut cmd = Command::new(&input[0]);
958    cmd.args(&input[1..]);
959
960    debug!(target: "cheatcodes", ?cmd, "invoking ffi");
961
962    cmd.current_dir(&state.config.root)
963        .output()
964        .map_err(|err| fmt_err!("failed to execute command {cmd:?}: {err}"))
965}
966
967fn prompt_input(prompt_text: &str) -> Result<String, dialoguer::Error> {
968    Input::new().allow_empty(true).with_prompt(prompt_text).interact_text()
969}
970
971fn prompt_password(prompt_text: &str) -> Result<String, dialoguer::Error> {
972    Password::new().with_prompt(prompt_text).interact()
973}
974
975fn prompt<FEN: FoundryEvmNetwork>(
976    state: &Cheatcodes<FEN>,
977    prompt_text: &str,
978    input: fn(&str) -> Result<String, dialoguer::Error>,
979) -> Result<String> {
980    let text_clone = prompt_text.to_string();
981    let timeout = state.config.prompt_timeout;
982    let (tx, rx) = mpsc::channel();
983
984    thread::spawn(move || {
985        let _ = tx.send(input(&text_clone));
986    });
987
988    match rx.recv_timeout(timeout) {
989        Ok(res) => res.map_err(|err| {
990            let _ = sh_println!();
991            err.to_string().into()
992        }),
993        Err(_) => {
994            let _ = sh_eprintln!();
995            Err("Prompt timed out".into())
996        }
997    }
998}
999
1000impl Cheatcode for getBroadcastCall {
1001    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
1002        let Self { contractName, chainId, txType } = self;
1003
1004        let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
1005            contractName,
1006            *chainId,
1007            &state.config.broadcast,
1008            vec![map_broadcast_tx_type(*txType)],
1009        )?;
1010
1011        Ok(latest_broadcast.abi_encode())
1012    }
1013}
1014
1015impl Cheatcode for getBroadcasts_0Call {
1016    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
1017        let Self { contractName, chainId, txType } = self;
1018
1019        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
1020            .with_tx_type(map_broadcast_tx_type(*txType));
1021
1022        let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
1023
1024        let summaries = broadcasts
1025            .into_iter()
1026            .flat_map(|broadcast| {
1027                let results = reader.into_tx_receipts(broadcast);
1028                parse_broadcast_results(results)
1029            })
1030            .collect::<Vec<_>>();
1031
1032        Ok(summaries.abi_encode())
1033    }
1034}
1035
1036impl Cheatcode for getBroadcasts_1Call {
1037    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
1038        let Self { contractName, chainId } = self;
1039
1040        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?;
1041
1042        let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
1043
1044        let summaries = broadcasts
1045            .into_iter()
1046            .flat_map(|broadcast| {
1047                let results = reader.into_tx_receipts(broadcast);
1048                parse_broadcast_results(results)
1049            })
1050            .collect::<Vec<_>>();
1051
1052        Ok(summaries.abi_encode())
1053    }
1054}
1055
1056impl Cheatcode for getDeployment_0Call {
1057    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
1058        let Self { contractName } = self;
1059        let chain_id = ccx.ecx.cfg().chain_id();
1060
1061        let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
1062            contractName,
1063            chain_id,
1064            &ccx.state.config.broadcast,
1065            vec![CallKind::Create, CallKind::Create2],
1066        )?;
1067
1068        Ok(latest_broadcast.contractAddress.abi_encode())
1069    }
1070}
1071
1072impl Cheatcode for getDeployment_1Call {
1073    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
1074        let Self { contractName, chainId } = self;
1075
1076        let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
1077            contractName,
1078            *chainId,
1079            &state.config.broadcast,
1080            vec![CallKind::Create, CallKind::Create2],
1081        )?;
1082
1083        Ok(latest_broadcast.contractAddress.abi_encode())
1084    }
1085}
1086
1087impl Cheatcode for getDeploymentsCall {
1088    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
1089        let Self { contractName, chainId } = self;
1090
1091        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
1092            .with_tx_type(CallKind::Create)
1093            .with_tx_type(CallKind::Create2);
1094
1095        let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
1096
1097        let summaries = broadcasts
1098            .into_iter()
1099            .flat_map(|broadcast| {
1100                let results = reader.into_tx_receipts(broadcast);
1101                parse_broadcast_results(results)
1102            })
1103            .collect::<Vec<_>>();
1104
1105        let deployed_addresses =
1106            summaries.into_iter().map(|summary| summary.contractAddress).collect::<Vec<_>>();
1107
1108        Ok(deployed_addresses.abi_encode())
1109    }
1110}
1111
1112fn map_broadcast_tx_type(tx_type: BroadcastTxType) -> CallKind {
1113    match tx_type {
1114        BroadcastTxType::Call => CallKind::Call,
1115        BroadcastTxType::Create => CallKind::Create,
1116        BroadcastTxType::Create2 => CallKind::Create2,
1117        _ => unreachable!("invalid tx type"),
1118    }
1119}
1120
1121fn parse_broadcast_results<N: Network>(
1122    results: Vec<(TransactionWithMetadata<N>, N::ReceiptResponse)>,
1123) -> Vec<BroadcastTxSummary> {
1124    results
1125        .into_iter()
1126        .map(|(tx, receipt)| BroadcastTxSummary {
1127            txHash: receipt.transaction_hash(),
1128            blockNumber: receipt.block_number().unwrap_or_default(),
1129            txType: match tx.call_kind {
1130                CallKind::Call => BroadcastTxType::Call,
1131                CallKind::Create => BroadcastTxType::Create,
1132                CallKind::Create2 => BroadcastTxType::Create2,
1133                _ => unreachable!("invalid tx type"),
1134            },
1135            contractAddress: tx.contract_address.unwrap_or_default(),
1136            success: receipt.status(),
1137        })
1138        .collect()
1139}
1140
1141fn latest_broadcast<N: Network>(
1142    contract_name: &String,
1143    chain_id: u64,
1144    broadcast_path: &Path,
1145    filters: Vec<CallKind>,
1146) -> Result<BroadcastTxSummary>
1147where
1148    N::TxEnvelope: for<'d> serde::Deserialize<'d>,
1149{
1150    let mut reader = BroadcastReader::new(contract_name.clone(), chain_id, broadcast_path)?;
1151
1152    for filter in filters {
1153        reader = reader.with_tx_type(filter);
1154    }
1155
1156    let broadcast = reader.read_latest::<N>()?;
1157
1158    let results = reader.into_tx_receipts(broadcast);
1159
1160    let summaries = parse_broadcast_results(results);
1161
1162    summaries
1163        .first()
1164        .ok_or_else(|| fmt_err!("no deployment found for {contract_name} on chain {chain_id}"))
1165        .cloned()
1166}
1167
1168#[cfg(test)]
1169mod tests {
1170    use super::*;
1171    use crate::CheatsConfig;
1172    use alloy_primitives::{address, b256};
1173    use foundry_common::ContractsByArtifact;
1174    use foundry_compilers::{
1175        ArtifactId,
1176        artifacts::{
1177            BytecodeObject, CompactBytecode, CompactContractBytecode, remappings::Remapping,
1178        },
1179    };
1180    use foundry_evm_core::evm::TempoEvmNetwork;
1181    use std::{env, fs as stdfs, str::FromStr, sync::Arc};
1182    use tempfile::TempDir;
1183
1184    fn cheats() -> Cheatcodes {
1185        let config = CheatsConfig {
1186            ffi: true,
1187            root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1188            ..Default::default()
1189        };
1190        Cheatcodes::new(Arc::new(config))
1191    }
1192
1193    #[test]
1194    fn test_ffi_hex() {
1195        let msg = b"gm";
1196        let cheats = cheats();
1197        let args = ["echo".to_string(), hex::encode(msg)];
1198        let output = ffi(&cheats, &args).unwrap();
1199        assert_eq!(output.stdout, Bytes::from(msg));
1200    }
1201
1202    #[test]
1203    fn test_ffi_string() {
1204        let msg = "gm";
1205        let cheats = cheats();
1206        let args = ["echo".to_string(), msg.to_string()];
1207        let output = ffi(&cheats, &args).unwrap();
1208        assert_eq!(output.stdout, Bytes::from(msg.as_bytes()));
1209    }
1210
1211    #[test]
1212    fn test_ffi_fails_on_error_code() {
1213        let mut cheats = cheats();
1214
1215        // Use a command that is guaranteed to fail with a non-zero exit code on any platform.
1216        #[cfg(unix)]
1217        let args = vec!["false".to_string()];
1218        #[cfg(windows)]
1219        let args = vec!["cmd".to_string(), "/c".to_string(), "exit 1".to_string()];
1220
1221        let result = Cheatcode::apply(&ffiCall { commandInput: args }, &mut cheats);
1222
1223        // Assert that the cheatcode returned an error.
1224        assert!(result.is_err(), "Expected ffi cheatcode to fail, but it succeeded");
1225
1226        // Assert that the error message contains the expected information.
1227        let err_msg = result.unwrap_err().to_string();
1228        assert!(
1229            err_msg.contains("exited with code 1"),
1230            "Error message did not contain exit code: {err_msg}"
1231        );
1232    }
1233
1234    #[test]
1235    fn test_artifact_parsing() {
1236        let s = include_str!("../../evm/test-data/solc-obj.json");
1237        let artifact: ContractObject = serde_json::from_str(s).unwrap();
1238        assert!(artifact.bytecode.is_some());
1239
1240        let artifact: ContractObject = serde_json::from_str(s).unwrap();
1241        assert!(artifact.deployed_bytecode.is_some());
1242    }
1243
1244    #[test]
1245    fn test_alloy_json_abi_rejects_unlinked_bytecode() {
1246        let artifact_json = r#"{
1247            "abi": [],
1248            "bytecode": "0x73__$987e73aeca5e61ce83e4cb0814d87beda9$__63baf2f868"
1249        }"#;
1250
1251        let result: Result<ContractObject, _> = serde_json::from_str(artifact_json);
1252        assert!(result.is_err(), "should reject unlinked bytecode with placeholders");
1253        let err = result.unwrap_err().to_string();
1254        assert!(err.contains("expected bytecode, found unlinked bytecode with placeholder"));
1255    }
1256
1257    #[test]
1258    fn test_parse_artifact_path_file_only() {
1259        let parsed = super::parse_artifact_path("path/to/Contract.sol").unwrap();
1260        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1261        assert_eq!(parsed.contract_name, None);
1262        assert_eq!(parsed.version, None);
1263        assert_eq!(parsed.profile, None);
1264    }
1265
1266    #[test]
1267    fn test_parse_artifact_path_windows_drive() {
1268        for file in [
1269            "C:/project/src/Contract.sol",
1270            r"C:\project\src\Contract.sol",
1271            r"\\?\C:\project\src\Contract.sol",
1272            r"\\server\share\Contract.sol",
1273        ] {
1274            for (suffix, contract_name, version, profile) in [
1275                ("", None, None, None),
1276                (":MyContract", Some("MyContract"), None, None),
1277                (":0.8.23", None, Some(Version::new(0, 8, 23)), None),
1278                (":MyContract:0.8.23", Some("MyContract"), Some(Version::new(0, 8, 23)), None),
1279                (":MyContract:optimized", Some("MyContract"), None, Some("optimized")),
1280            ] {
1281                let input = format!("{file}{suffix}");
1282                assert_eq!(
1283                    parse_artifact_path(&input).unwrap(),
1284                    ParsedArtifactPath {
1285                        file: Some(PathBuf::from(file)),
1286                        contract_name,
1287                        version,
1288                        profile,
1289                    },
1290                    "{input}"
1291                );
1292            }
1293        }
1294
1295        // A one-letter contract name must still allow version and profile suffixes.
1296        assert_eq!(parse_artifact_path("C:0.8.23").unwrap().version, Some(Version::new(0, 8, 23)));
1297        assert_eq!(parse_artifact_path("C:optimized").unwrap().profile, Some("optimized"));
1298    }
1299
1300    #[test]
1301    fn test_parse_artifact_path_file_and_contract() {
1302        let parsed = super::parse_artifact_path("path/to/Contract.sol:MyContract").unwrap();
1303        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1304        assert_eq!(parsed.contract_name, Some("MyContract"));
1305        assert_eq!(parsed.version, None);
1306        assert_eq!(parsed.profile, None);
1307    }
1308
1309    #[test]
1310    fn test_parse_artifact_path_file_contract_version() {
1311        let parsed = super::parse_artifact_path("path/to/Contract.sol:MyContract:0.8.23").unwrap();
1312        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1313        assert_eq!(parsed.contract_name, Some("MyContract"));
1314        assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 23)));
1315        assert_eq!(parsed.profile, None);
1316    }
1317
1318    #[test]
1319    fn test_parse_artifact_path_file_contract_profile() {
1320        let parsed =
1321            super::parse_artifact_path("path/to/Contract.sol:MyContract:optimized").unwrap();
1322        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1323        assert_eq!(parsed.contract_name, Some("MyContract"));
1324        assert_eq!(parsed.version, None);
1325        assert_eq!(parsed.profile, Some("optimized"));
1326    }
1327
1328    #[test]
1329    fn test_parse_artifact_path_file_and_version() {
1330        let parsed = super::parse_artifact_path("path/to/Contract.sol:0.8.18").unwrap();
1331        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1332        assert_eq!(parsed.contract_name, None);
1333        assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 18)));
1334        assert_eq!(parsed.profile, None);
1335    }
1336
1337    #[test]
1338    fn test_parse_artifact_path_file_and_profile() {
1339        // The parser keeps the two-part file form ambiguous. Artifact lookup can resolve this
1340        // segment as a profile when no contract name matches.
1341        let parsed = super::parse_artifact_path("Contract.sol:paris").unwrap();
1342        assert_eq!(parsed.file, Some(PathBuf::from("Contract.sol")));
1343        assert_eq!(parsed.contract_name, Some("paris"));
1344        assert_eq!(parsed.version, None);
1345        assert_eq!(parsed.profile, None);
1346    }
1347
1348    fn test_artifact(
1349        source: &str,
1350        name: &str,
1351        profile: &str,
1352        bytecode: Bytes,
1353    ) -> (ArtifactId, CompactContractBytecode) {
1354        (
1355            ArtifactId {
1356                path: PathBuf::from(format!("{source}/{name}.json")),
1357                name: name.to_owned(),
1358                source: PathBuf::from(source),
1359                version: Version::new(0, 8, 30),
1360                build_id: String::new(),
1361                profile: profile.to_owned(),
1362            },
1363            CompactContractBytecode {
1364                abi: Some(Default::default()),
1365                bytecode: Some(CompactBytecode {
1366                    object: BytecodeObject::Bytecode(bytecode),
1367                    source_map: None,
1368                    link_references: Default::default(),
1369                }),
1370                deployed_bytecode: None,
1371            },
1372        )
1373    }
1374
1375    #[test]
1376    fn test_get_artifact_code_resolves_file_profile_ambiguity() {
1377        let default_bytecode = Bytes::from_static(&[0x60, 0x01]);
1378        let paris_bytecode = Bytes::from_static(&[0x60, 0x02]);
1379        let source = "src/GetCodeProfile.t.sol";
1380        let artifacts = ContractsByArtifact::new([
1381            test_artifact(source, "GetCodeProfile", "default", default_bytecode),
1382            test_artifact(source, "GetCodeProfile", "paris", paris_bytecode.clone()),
1383        ]);
1384        let config = CheatsConfig {
1385            available_artifacts: Some(artifacts),
1386            root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1387            ..Default::default()
1388        };
1389        let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1390
1391        let bytecode =
1392            super::get_artifact_code(&cheats, "src/GetCodeProfile.t.sol:paris", false).unwrap();
1393
1394        assert_eq!(bytecode, paris_bytecode);
1395    }
1396
1397    #[test]
1398    fn test_get_artifact_code_prefers_contract_name_over_file_profile_ambiguity() {
1399        let profile_bytecode = Bytes::from_static(&[0x60, 0x02]);
1400        let contract_bytecode = Bytes::from_static(&[0x60, 0x03]);
1401        let source = "src/GetCodeProfile.t.sol";
1402        let artifacts = ContractsByArtifact::new([
1403            test_artifact(source, "GetCodeProfile", "paris", profile_bytecode),
1404            test_artifact(source, "paris", "default", contract_bytecode.clone()),
1405        ]);
1406        let config = CheatsConfig {
1407            available_artifacts: Some(artifacts),
1408            root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1409            ..Default::default()
1410        };
1411        let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1412
1413        let bytecode =
1414            super::get_artifact_code(&cheats, "src/GetCodeProfile.t.sol:paris", false).unwrap();
1415
1416        assert_eq!(bytecode, contract_bytecode);
1417    }
1418
1419    #[test]
1420    fn test_get_artifact_code_resolves_remapping() {
1421        let bytecode = Bytes::from_static(&[0x60, 0x01]);
1422        let artifacts = ContractsByArtifact::new([test_artifact(
1423            "src/Something.sol",
1424            "Something",
1425            "default",
1426            bytecode.clone(),
1427        )]);
1428        let root = PathBuf::from(&env!("CARGO_MANIFEST_DIR"));
1429        let paths = foundry_compilers::ProjectPathsConfig::builder()
1430            .remapping(Remapping::from_str("@example/=src/").unwrap())
1431            .build_with_root(&root);
1432        let config = CheatsConfig {
1433            available_artifacts: Some(artifacts),
1434            root,
1435            paths,
1436            ..Default::default()
1437        };
1438        let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1439
1440        let resolved =
1441            super::get_artifact_code(&cheats, "@example/Something.sol:Something", false).unwrap();
1442
1443        assert_eq!(resolved, bytecode);
1444    }
1445
1446    #[test]
1447    fn test_get_artifact_code_preserves_project_path_on_library_collision() {
1448        let root_bytecode = Bytes::from_static(&[0x60, 0x01]);
1449        let library_bytecode = Bytes::from_static(&[0x60, 0x02]);
1450        let artifacts = ContractsByArtifact::new([
1451            test_artifact("src/Thing.sol", "RootThing", "default", root_bytecode.clone()),
1452            test_artifact("lib/src/Thing.sol", "LibThing", "default", library_bytecode),
1453        ]);
1454        let temp = TempDir::new().unwrap();
1455        let library = temp.path().join("lib");
1456        stdfs::create_dir_all(library.join("src")).unwrap();
1457        stdfs::write(library.join("src/Thing.sol"), "").unwrap();
1458        let paths = foundry_compilers::ProjectPathsConfig::builder()
1459            .remappings([])
1460            .lib(library)
1461            .build_with_root(temp.path());
1462        let config = CheatsConfig {
1463            available_artifacts: Some(artifacts),
1464            root: temp.path().to_path_buf(),
1465            paths,
1466            ..Default::default()
1467        };
1468        let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1469
1470        let resolved = super::get_artifact_code(&cheats, "src/Thing.sol:RootThing", false).unwrap();
1471
1472        assert_eq!(resolved, root_bytecode);
1473    }
1474
1475    #[test]
1476    fn test_parse_artifact_path_contract_only() {
1477        let parsed = super::parse_artifact_path("MyContract").unwrap();
1478        assert_eq!(parsed.file, None);
1479        assert_eq!(parsed.contract_name, Some("MyContract"));
1480        assert_eq!(parsed.version, None);
1481        assert_eq!(parsed.profile, None);
1482    }
1483
1484    #[test]
1485    fn test_parse_artifact_path_contract_and_version() {
1486        let parsed = super::parse_artifact_path("MyContract:0.8.23").unwrap();
1487        assert_eq!(parsed.file, None);
1488        assert_eq!(parsed.contract_name, Some("MyContract"));
1489        assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 23)));
1490        assert_eq!(parsed.profile, None);
1491    }
1492
1493    #[test]
1494    fn test_parse_artifact_path_contract_and_profile() {
1495        let parsed = super::parse_artifact_path("MyContract:optimized").unwrap();
1496        assert_eq!(parsed.file, None);
1497        assert_eq!(parsed.contract_name, Some("MyContract"));
1498        assert_eq!(parsed.version, None);
1499        assert_eq!(parsed.profile, Some("optimized"));
1500    }
1501
1502    #[test]
1503    fn test_parse_artifact_path_profile_names() {
1504        // Test various profile name patterns
1505        for profile in ["v1", "v2", "paris", "optimized", "default", "prod", "dev"] {
1506            let path = format!("MyContract:{profile}");
1507            let parsed = super::parse_artifact_path(&path).unwrap();
1508            assert_eq!(parsed.contract_name, Some("MyContract"));
1509            assert_eq!(parsed.profile, Some(profile));
1510            assert_eq!(parsed.version, None);
1511        }
1512    }
1513
1514    #[test]
1515    fn test_parse_artifact_path_invalid_version() {
1516        // Invalid semver should be treated as profile
1517        let parsed = super::parse_artifact_path("MyContract:invalid").unwrap();
1518        assert_eq!(parsed.contract_name, Some("MyContract"));
1519        assert_eq!(parsed.profile, Some("invalid"));
1520        assert_eq!(parsed.version, None);
1521    }
1522
1523    fn unique_temp_dir(prefix: &str) -> PathBuf {
1524        env::temp_dir().join(format!(
1525            "foundry-cheatcodes-{prefix}-{}",
1526            SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos()
1527        ))
1528    }
1529
1530    #[test]
1531    fn test_latest_broadcast_reads_tempo_sequences() {
1532        let root = unique_temp_dir("tempo-broadcast");
1533        let broadcast_path = root.join("broadcast");
1534        let sequence_dir = broadcast_path.join("Counter.s.sol").join("31337");
1535        stdfs::create_dir_all(&sequence_dir).unwrap();
1536
1537        let tx_hash = "0x04548a0ea27e2cccc1479af3c2ff02da4d4d3ea46af8e8d7edaa49f6ea27073f";
1538        let block_hash = "0x860f788b251ece768e63b0d3906d156f652d843848b71c7fe81faacd49139d66";
1539        let from = "0xa70ab0448e66cd77995bfbba5c5b64b41a85f3fd";
1540        let contract_address = "0x20c0000000000000000000000000000000000000";
1541        let zero_bloom = format!("0x{}", "0".repeat(512));
1542
1543        let sequence = serde_json::json!({
1544            "transactions": [{
1545                "hash": tx_hash,
1546                "transactionType": "CREATE",
1547                "contractName": "Counter",
1548                "contractAddress": contract_address,
1549                "function": serde_json::Value::Null,
1550                "arguments": serde_json::Value::Null,
1551                "transaction": {
1552                    "type": "0x76",
1553                    "from": from,
1554                    "to": serde_json::Value::Null,
1555                    "data": "0x",
1556                    "value": "0x0",
1557                    "gas": "0x5208",
1558                    "nonce": "0x0",
1559                    "accessList": [],
1560                    "calls": [],
1561                    "nonceKey": "0x0",
1562                    "feePayerSignature": serde_json::Value::Null,
1563                    "validBefore": serde_json::Value::Null,
1564                    "validAfter": serde_json::Value::Null,
1565                    "keyAuthorization": serde_json::Value::Null,
1566                    "aaAuthorizationList": []
1567                },
1568                "additionalContracts": [],
1569                "isFixedGasLimit": false
1570            }],
1571            "receipts": [{
1572                "type": "0x76",
1573                "status": "0x1",
1574                "cumulativeGasUsed": "0x5208",
1575                "logs": [],
1576                "logsBloom": zero_bloom,
1577                "transactionHash": tx_hash,
1578                "transactionIndex": "0x0",
1579                "blockHash": block_hash,
1580                "blockNumber": "0x7",
1581                "gasUsed": "0x5208",
1582                "effectiveGasPrice": "0x1",
1583                "from": from,
1584                "to": serde_json::Value::Null,
1585                "contractAddress": contract_address,
1586                "feePayer": from
1587            }],
1588            "libraries": [],
1589            "pending": [],
1590            "returns": {},
1591            "timestamp": 1,
1592            "chain": 31337,
1593            "commit": serde_json::Value::Null
1594        });
1595
1596        fs::write_json_file(&sequence_dir.join("run-1.json"), &sequence).unwrap();
1597
1598        let latest = latest_broadcast::<<TempoEvmNetwork as FoundryEvmNetwork>::Network>(
1599            &"Counter".to_owned(),
1600            31337,
1601            &broadcast_path,
1602            vec![CallKind::Create],
1603        )
1604        .unwrap();
1605
1606        assert_eq!(
1607            latest.txHash,
1608            b256!("04548a0ea27e2cccc1479af3c2ff02da4d4d3ea46af8e8d7edaa49f6ea27073f")
1609        );
1610        assert_eq!(latest.blockNumber, 7);
1611        assert!(matches!(latest.txType, BroadcastTxType::Create));
1612        assert_eq!(latest.contractAddress, address!("20c0000000000000000000000000000000000000"));
1613        assert!(latest.success);
1614
1615        stdfs::remove_dir_all(root).unwrap();
1616    }
1617}