foundry_cheatcodes/
config.rs

1use super::Result;
2use crate::Vm::Rpc;
3use alloy_primitives::{map::AddressHashMap, U256};
4use foundry_common::{fs::normalize_path, ContractsByArtifact};
5use foundry_compilers::{utils::canonicalize, ArtifactId, ProjectPathsConfig};
6use foundry_config::{
7    cache::StorageCachingConfig, fs_permissions::FsAccessKind, Config, FsPermissions,
8    ResolvedRpcEndpoint, ResolvedRpcEndpoints, RpcEndpoint, RpcEndpointUrl,
9};
10use foundry_evm_core::opts::EvmOpts;
11use std::{
12    collections::HashMap,
13    path::{Path, PathBuf},
14    time::Duration,
15};
16
17/// Additional, configurable context the `Cheatcodes` inspector has access to
18///
19/// This is essentially a subset of various `Config` settings `Cheatcodes` needs to know.
20#[derive(Clone, Debug)]
21pub struct CheatsConfig {
22    /// Whether the FFI cheatcode is enabled.
23    pub ffi: bool,
24    /// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
25    pub always_use_create_2_factory: bool,
26    /// Sets a timeout for vm.prompt cheatcodes
27    pub prompt_timeout: Duration,
28    /// RPC storage caching settings determines what chains and endpoints to cache
29    pub rpc_storage_caching: StorageCachingConfig,
30    /// Disables storage caching entirely.
31    pub no_storage_caching: bool,
32    /// All known endpoints and their aliases
33    pub rpc_endpoints: ResolvedRpcEndpoints,
34    /// Project's paths as configured
35    pub paths: ProjectPathsConfig,
36    /// Path to the directory that contains the bindings generated by `forge bind-json`.
37    pub bind_json_path: PathBuf,
38    /// Filesystem permissions for cheatcodes like `writeFile`, `readFile`
39    pub fs_permissions: FsPermissions,
40    /// Project root
41    pub root: PathBuf,
42    /// Absolute Path to broadcast dir i.e project_root/broadcast
43    pub broadcast: PathBuf,
44    /// Paths (directories) where file reading/writing is allowed
45    pub allowed_paths: Vec<PathBuf>,
46    /// How the evm was configured by the user
47    pub evm_opts: EvmOpts,
48    /// Address labels from config
49    pub labels: AddressHashMap<String>,
50    /// Artifacts which are guaranteed to be fresh (either recompiled or cached).
51    /// If Some, `vm.getDeployedCode` invocations are validated to be in scope of this list.
52    /// If None, no validation is performed.
53    pub available_artifacts: Option<ContractsByArtifact>,
54    /// Currently running artifact.
55    pub running_artifact: Option<ArtifactId>,
56    /// Whether to enable legacy (non-reverting) assertions.
57    pub assertions_revert: bool,
58    /// Optional seed for the RNG algorithm.
59    pub seed: Option<U256>,
60    /// Whether to allow `expectRevert` to work for internal calls.
61    pub internal_expect_revert: bool,
62    /// Mapping of chain aliases to chain data
63    pub chains: HashMap<String, ChainData>,
64    /// Mapping of chain IDs to their aliases
65    pub chain_id_to_alias: HashMap<u64, String>,
66}
67
68/// Chain data for getChain cheatcodes
69#[derive(Clone, Debug)]
70pub struct ChainData {
71    pub name: String,
72    pub chain_id: u64,
73    pub default_rpc_url: String, // Store default RPC URL
74}
75
76impl CheatsConfig {
77    /// Extracts the necessary settings from the Config
78    pub fn new(
79        config: &Config,
80        evm_opts: EvmOpts,
81        available_artifacts: Option<ContractsByArtifact>,
82        running_artifact: Option<ArtifactId>,
83    ) -> Self {
84        let mut allowed_paths = vec![config.root.clone()];
85        allowed_paths.extend(config.libs.iter().cloned());
86        allowed_paths.extend(config.allow_paths.iter().cloned());
87
88        let rpc_endpoints = config.rpc_endpoints.clone().resolved();
89        trace!(?rpc_endpoints, "using resolved rpc endpoints");
90
91        // If user explicitly disabled safety checks, do not set available_artifacts
92        let available_artifacts =
93            if config.unchecked_cheatcode_artifacts { None } else { available_artifacts };
94
95        Self {
96            ffi: evm_opts.ffi,
97            always_use_create_2_factory: evm_opts.always_use_create_2_factory,
98            prompt_timeout: Duration::from_secs(config.prompt_timeout),
99            rpc_storage_caching: config.rpc_storage_caching.clone(),
100            no_storage_caching: config.no_storage_caching,
101            rpc_endpoints,
102            paths: config.project_paths(),
103            bind_json_path: config.bind_json.out.clone(),
104            fs_permissions: config.fs_permissions.clone().joined(config.root.as_ref()),
105            root: config.root.clone(),
106            broadcast: config.root.clone().join(&config.broadcast),
107            allowed_paths,
108            evm_opts,
109            labels: config.labels.clone(),
110            available_artifacts,
111            running_artifact,
112            assertions_revert: config.assertions_revert,
113            seed: config.fuzz.seed,
114            internal_expect_revert: config.allow_internal_expect_revert,
115            chains: HashMap::new(),
116            chain_id_to_alias: HashMap::new(),
117        }
118    }
119
120    /// Returns a new `CheatsConfig` configured with the given `Config` and `EvmOpts`.
121    pub fn clone_with(&self, config: &Config, evm_opts: EvmOpts) -> Self {
122        Self::new(config, evm_opts, self.available_artifacts.clone(), self.running_artifact.clone())
123    }
124
125    /// Attempts to canonicalize (see [std::fs::canonicalize]) the path.
126    ///
127    /// Canonicalization fails for non-existing paths, in which case we just normalize the path.
128    pub fn normalized_path(&self, path: impl AsRef<Path>) -> PathBuf {
129        let path = self.root.join(path);
130        canonicalize(&path).unwrap_or_else(|_| normalize_path(&path))
131    }
132
133    /// Returns true if the given path is allowed, if any path `allowed_paths` is an ancestor of the
134    /// path
135    ///
136    /// We only allow paths that are inside  allowed paths. To prevent path traversal
137    /// ("../../etc/passwd") we canonicalize/normalize the path first. We always join with the
138    /// configured root directory.
139    pub fn is_path_allowed(&self, path: impl AsRef<Path>, kind: FsAccessKind) -> bool {
140        self.is_normalized_path_allowed(&self.normalized_path(path), kind)
141    }
142
143    fn is_normalized_path_allowed(&self, path: &Path, kind: FsAccessKind) -> bool {
144        self.fs_permissions.is_path_allowed(path, kind)
145    }
146
147    /// Returns an error if no access is granted to access `path`, See also [Self::is_path_allowed]
148    ///
149    /// Returns the normalized version of `path`, see [`CheatsConfig::normalized_path`]
150    pub fn ensure_path_allowed(
151        &self,
152        path: impl AsRef<Path>,
153        kind: FsAccessKind,
154    ) -> Result<PathBuf> {
155        let path = path.as_ref();
156        let normalized = self.normalized_path(path);
157        ensure!(
158            self.is_normalized_path_allowed(&normalized, kind),
159            "the path {} is not allowed to be accessed for {kind} operations",
160            normalized.strip_prefix(&self.root).unwrap_or(path).display()
161        );
162        Ok(normalized)
163    }
164
165    /// Returns true if the given `path` is the project's foundry.toml file
166    ///
167    /// Note: this should be called with normalized path
168    pub fn is_foundry_toml(&self, path: impl AsRef<Path>) -> bool {
169        // path methods that do not access the filesystem are such as [`Path::starts_with`], are
170        // case-sensitive no matter the platform or filesystem. to make this case-sensitive
171        // we convert the underlying `OssStr` to lowercase checking that `path` and
172        // `foundry.toml` are the same file by comparing the FD, because it may not exist
173        let foundry_toml = self.root.join(Config::FILE_NAME);
174        Path::new(&foundry_toml.to_string_lossy().to_lowercase())
175            .starts_with(Path::new(&path.as_ref().to_string_lossy().to_lowercase()))
176    }
177
178    /// Same as [`Self::is_foundry_toml`] but returns an `Err` if [`Self::is_foundry_toml`] returns
179    /// true
180    pub fn ensure_not_foundry_toml(&self, path: impl AsRef<Path>) -> Result<()> {
181        ensure!(!self.is_foundry_toml(path), "access to `foundry.toml` is not allowed");
182        Ok(())
183    }
184
185    /// Returns the RPC to use
186    ///
187    /// If `url_or_alias` is a known alias in the `ResolvedRpcEndpoints` then it returns the
188    /// corresponding URL of that alias. otherwise this assumes `url_or_alias` is itself a URL
189    /// if it starts with a `http` or `ws` scheme.
190    ///
191    /// If the url is a path to an existing file, it is also considered a valid RPC URL, IPC path.
192    ///
193    /// # Errors
194    ///
195    ///  - Returns an error if `url_or_alias` is a known alias but references an unresolved env var.
196    ///  - Returns an error if `url_or_alias` is not an alias but does not start with a `http` or
197    ///    `ws` `scheme` and is not a path to an existing file
198    pub fn rpc_endpoint(&self, url_or_alias: &str) -> Result<ResolvedRpcEndpoint> {
199        if let Some(endpoint) = self.rpc_endpoints.get(url_or_alias) {
200            Ok(endpoint.clone().try_resolve())
201        } else {
202            // check if it's a URL or a path to an existing file to an ipc socket
203            if url_or_alias.starts_with("http") ||
204                url_or_alias.starts_with("ws") ||
205                // check for existing ipc file
206                Path::new(url_or_alias).exists()
207            {
208                let url = RpcEndpointUrl::Env(url_or_alias.to_string());
209                Ok(RpcEndpoint::new(url).resolve())
210            } else {
211                Err(fmt_err!("invalid rpc url: {url_or_alias}"))
212            }
213        }
214    }
215    /// Returns all the RPC urls and their alias.
216    pub fn rpc_urls(&self) -> Result<Vec<Rpc>> {
217        let mut urls = Vec::with_capacity(self.rpc_endpoints.len());
218        for alias in self.rpc_endpoints.keys() {
219            let url = self.rpc_endpoint(alias)?.url()?;
220            urls.push(Rpc { key: alias.clone(), url });
221        }
222        Ok(urls)
223    }
224
225    /// Initialize default chain data (similar to initializeStdChains in Solidity)
226    pub fn initialize_chain_data(&mut self) {
227        if !self.chains.is_empty() {
228            return; // Already initialized
229        }
230
231        // Use the same function to create chains
232        let chains = create_default_chains();
233
234        // Add all chains to the config
235        for (alias, data) in chains {
236            self.set_chain_with_default_rpc_url(&alias, data);
237        }
238    }
239
240    /// Set chain with default RPC URL (similar to setChainWithDefaultRpcUrl in Solidity)
241    pub fn set_chain_with_default_rpc_url(&mut self, alias: &str, data: ChainData) {
242        // Store the default RPC URL is already stored in the data
243        // No need to clone it separately
244
245        // Add chain data
246        self.set_chain_data(alias, data);
247    }
248
249    /// Set chain data for a specific alias
250    pub fn set_chain_data(&mut self, alias: &str, data: ChainData) {
251        // Remove old chain ID mapping if it exists
252        if let Some(old_data) = self.chains.get(alias) {
253            self.chain_id_to_alias.remove(&old_data.chain_id);
254        }
255
256        // Add new mappings
257        self.chain_id_to_alias.insert(data.chain_id, alias.to_string());
258        self.chains.insert(alias.to_string(), data);
259    }
260
261    /// Get chain data by alias
262    pub fn get_chain_data_by_alias_non_mut(&self, alias: &str) -> Result<ChainData> {
263        // Initialize chains if not already done
264        if self.chains.is_empty() {
265            // Create a temporary copy with initialized chains
266            // This is inefficient but handles the edge case
267            let temp_chains = create_default_chains();
268
269            if let Some(data) = temp_chains.get(alias) {
270                return Ok(data.clone());
271            }
272        } else {
273            // Normal path - chains are initialized
274            if let Some(data) = self.chains.get(alias) {
275                return Ok(data.clone());
276            }
277        }
278
279        // Chain not found in either case
280        Err(fmt_err!("vm.getChain: Chain with alias \"{}\" not found", alias))
281    }
282
283    /// Get RPC URL for an alias
284    pub fn get_rpc_url_non_mut(&self, alias: &str) -> Result<String> {
285        // Try to get from config first
286        match self.rpc_endpoint(alias) {
287            Ok(endpoint) => Ok(endpoint.url()?),
288            Err(_) => {
289                // If not in config, try to get default URL
290                let chain_data = self.get_chain_data_by_alias_non_mut(alias)?;
291                Ok(chain_data.default_rpc_url)
292            }
293        }
294    }
295}
296
297impl Default for CheatsConfig {
298    fn default() -> Self {
299        Self {
300            ffi: false,
301            always_use_create_2_factory: false,
302            prompt_timeout: Duration::from_secs(120),
303            rpc_storage_caching: Default::default(),
304            no_storage_caching: false,
305            rpc_endpoints: Default::default(),
306            paths: ProjectPathsConfig::builder().build_with_root("./"),
307            fs_permissions: Default::default(),
308            root: Default::default(),
309            bind_json_path: PathBuf::default().join("utils").join("jsonBindings.sol"),
310            broadcast: Default::default(),
311            allowed_paths: vec![],
312            evm_opts: Default::default(),
313            labels: Default::default(),
314            available_artifacts: Default::default(),
315            running_artifact: Default::default(),
316            assertions_revert: true,
317            seed: None,
318            internal_expect_revert: false,
319            chains: HashMap::new(),
320            chain_id_to_alias: HashMap::new(),
321        }
322    }
323}
324
325// Helper function to set default chains
326fn create_default_chains() -> HashMap<String, ChainData> {
327    let mut chains = HashMap::new();
328
329    // Define all chains in one place
330    chains.insert(
331        "anvil".to_string(),
332        ChainData {
333            name: "Anvil".to_string(),
334            chain_id: 31337,
335            default_rpc_url: "http://127.0.0.1:8545".to_string(),
336        },
337    );
338
339    chains.insert(
340        "mainnet".to_string(),
341        ChainData {
342            name: "Mainnet".to_string(),
343            chain_id: 1,
344            default_rpc_url: "https://eth.llamarpc.com".to_string(),
345        },
346    );
347
348    chains.insert(
349        "sepolia".to_string(),
350        ChainData {
351            name: "Sepolia".to_string(),
352            chain_id: 11155111,
353            default_rpc_url: "https://sepolia.infura.io/v3/b9794ad1ddf84dfb8c34d6bb5dca2001"
354                .to_string(),
355        },
356    );
357
358    chains.insert(
359        "holesky".to_string(),
360        ChainData {
361            name: "Holesky".to_string(),
362            chain_id: 17000,
363            default_rpc_url: "https://rpc.holesky.ethpandaops.io".to_string(),
364        },
365    );
366
367    chains.insert(
368        "optimism".to_string(),
369        ChainData {
370            name: "Optimism".to_string(),
371            chain_id: 10,
372            default_rpc_url: "https://mainnet.optimism.io".to_string(),
373        },
374    );
375
376    chains.insert(
377        "optimism_sepolia".to_string(),
378        ChainData {
379            name: "Optimism Sepolia".to_string(),
380            chain_id: 11155420,
381            default_rpc_url: "https://sepolia.optimism.io".to_string(),
382        },
383    );
384
385    chains.insert(
386        "arbitrum_one".to_string(),
387        ChainData {
388            name: "Arbitrum One".to_string(),
389            chain_id: 42161,
390            default_rpc_url: "https://arb1.arbitrum.io/rpc".to_string(),
391        },
392    );
393
394    chains.insert(
395        "arbitrum_one_sepolia".to_string(),
396        ChainData {
397            name: "Arbitrum One Sepolia".to_string(),
398            chain_id: 421614,
399            default_rpc_url: "https://sepolia-rollup.arbitrum.io/rpc".to_string(),
400        },
401    );
402
403    chains.insert(
404        "arbitrum_nova".to_string(),
405        ChainData {
406            name: "Arbitrum Nova".to_string(),
407            chain_id: 42170,
408            default_rpc_url: "https://nova.arbitrum.io/rpc".to_string(),
409        },
410    );
411
412    chains.insert(
413        "polygon".to_string(),
414        ChainData {
415            name: "Polygon".to_string(),
416            chain_id: 137,
417            default_rpc_url: "https://polygon-rpc.com".to_string(),
418        },
419    );
420
421    chains.insert(
422        "polygon_amoy".to_string(),
423        ChainData {
424            name: "Polygon Amoy".to_string(),
425            chain_id: 80002,
426            default_rpc_url: "https://rpc-amoy.polygon.technology".to_string(),
427        },
428    );
429
430    chains.insert(
431        "avalanche".to_string(),
432        ChainData {
433            name: "Avalanche".to_string(),
434            chain_id: 43114,
435            default_rpc_url: "https://api.avax.network/ext/bc/C/rpc".to_string(),
436        },
437    );
438
439    chains.insert(
440        "avalanche_fuji".to_string(),
441        ChainData {
442            name: "Avalanche Fuji".to_string(),
443            chain_id: 43113,
444            default_rpc_url: "https://api.avax-test.network/ext/bc/C/rpc".to_string(),
445        },
446    );
447
448    chains.insert(
449        "bnb_smart_chain".to_string(),
450        ChainData {
451            name: "BNB Smart Chain".to_string(),
452            chain_id: 56,
453            default_rpc_url: "https://bsc-dataseed1.binance.org".to_string(),
454        },
455    );
456
457    chains.insert(
458        "bnb_smart_chain_testnet".to_string(),
459        ChainData {
460            name: "BNB Smart Chain Testnet".to_string(),
461            chain_id: 97,
462            default_rpc_url: "https://rpc.ankr.com/bsc_testnet_chapel".to_string(),
463        },
464    );
465
466    chains.insert(
467        "gnosis_chain".to_string(),
468        ChainData {
469            name: "Gnosis Chain".to_string(),
470            chain_id: 100,
471            default_rpc_url: "https://rpc.gnosischain.com".to_string(),
472        },
473    );
474
475    chains.insert(
476        "moonbeam".to_string(),
477        ChainData {
478            name: "Moonbeam".to_string(),
479            chain_id: 1284,
480            default_rpc_url: "https://rpc.api.moonbeam.network".to_string(),
481        },
482    );
483
484    chains.insert(
485        "moonriver".to_string(),
486        ChainData {
487            name: "Moonriver".to_string(),
488            chain_id: 1285,
489            default_rpc_url: "https://rpc.api.moonriver.moonbeam.network".to_string(),
490        },
491    );
492
493    chains.insert(
494        "moonbase".to_string(),
495        ChainData {
496            name: "Moonbase".to_string(),
497            chain_id: 1287,
498            default_rpc_url: "https://rpc.testnet.moonbeam.network".to_string(),
499        },
500    );
501
502    chains.insert(
503        "base_sepolia".to_string(),
504        ChainData {
505            name: "Base Sepolia".to_string(),
506            chain_id: 84532,
507            default_rpc_url: "https://sepolia.base.org".to_string(),
508        },
509    );
510
511    chains.insert(
512        "base".to_string(),
513        ChainData {
514            name: "Base".to_string(),
515            chain_id: 8453,
516            default_rpc_url: "https://mainnet.base.org".to_string(),
517        },
518    );
519
520    chains.insert(
521        "blast_sepolia".to_string(),
522        ChainData {
523            name: "Blast Sepolia".to_string(),
524            chain_id: 168587773,
525            default_rpc_url: "https://sepolia.blast.io".to_string(),
526        },
527    );
528
529    chains.insert(
530        "blast".to_string(),
531        ChainData {
532            name: "Blast".to_string(),
533            chain_id: 81457,
534            default_rpc_url: "https://rpc.blast.io".to_string(),
535        },
536    );
537
538    chains.insert(
539        "fantom_opera".to_string(),
540        ChainData {
541            name: "Fantom Opera".to_string(),
542            chain_id: 250,
543            default_rpc_url: "https://rpc.ankr.com/fantom/".to_string(),
544        },
545    );
546
547    chains.insert(
548        "fantom_opera_testnet".to_string(),
549        ChainData {
550            name: "Fantom Opera Testnet".to_string(),
551            chain_id: 4002,
552            default_rpc_url: "https://rpc.ankr.com/fantom_testnet/".to_string(),
553        },
554    );
555
556    chains.insert(
557        "fraxtal".to_string(),
558        ChainData {
559            name: "Fraxtal".to_string(),
560            chain_id: 252,
561            default_rpc_url: "https://rpc.frax.com".to_string(),
562        },
563    );
564
565    chains.insert(
566        "fraxtal_testnet".to_string(),
567        ChainData {
568            name: "Fraxtal Testnet".to_string(),
569            chain_id: 2522,
570            default_rpc_url: "https://rpc.testnet.frax.com".to_string(),
571        },
572    );
573
574    chains.insert(
575        "berachain_bartio_testnet".to_string(),
576        ChainData {
577            name: "Berachain bArtio Testnet".to_string(),
578            chain_id: 80084,
579            default_rpc_url: "https://bartio.rpc.berachain.com".to_string(),
580        },
581    );
582
583    chains.insert(
584        "flare".to_string(),
585        ChainData {
586            name: "Flare".to_string(),
587            chain_id: 14,
588            default_rpc_url: "https://flare-api.flare.network/ext/C/rpc".to_string(),
589        },
590    );
591
592    chains.insert(
593        "flare_coston2".to_string(),
594        ChainData {
595            name: "Flare Coston2".to_string(),
596            chain_id: 114,
597            default_rpc_url: "https://coston2-api.flare.network/ext/C/rpc".to_string(),
598        },
599    );
600
601    chains.insert(
602        "mode".to_string(),
603        ChainData {
604            name: "Mode".to_string(),
605            chain_id: 34443,
606            default_rpc_url: "https://mode.drpc.org".to_string(),
607        },
608    );
609
610    chains.insert(
611        "mode_sepolia".to_string(),
612        ChainData {
613            name: "Mode Sepolia".to_string(),
614            chain_id: 919,
615            default_rpc_url: "https://sepolia.mode.network".to_string(),
616        },
617    );
618
619    chains.insert(
620        "zora".to_string(),
621        ChainData {
622            name: "Zora".to_string(),
623            chain_id: 7777777,
624            default_rpc_url: "https://zora.drpc.org".to_string(),
625        },
626    );
627
628    chains.insert(
629        "zora_sepolia".to_string(),
630        ChainData {
631            name: "Zora Sepolia".to_string(),
632            chain_id: 999999999,
633            default_rpc_url: "https://sepolia.rpc.zora.energy".to_string(),
634        },
635    );
636
637    chains.insert(
638        "race".to_string(),
639        ChainData {
640            name: "Race".to_string(),
641            chain_id: 6805,
642            default_rpc_url: "https://racemainnet.io".to_string(),
643        },
644    );
645
646    chains.insert(
647        "race_sepolia".to_string(),
648        ChainData {
649            name: "Race Sepolia".to_string(),
650            chain_id: 6806,
651            default_rpc_url: "https://racemainnet.io".to_string(),
652        },
653    );
654
655    chains.insert(
656        "metal".to_string(),
657        ChainData {
658            name: "Metal".to_string(),
659            chain_id: 1750,
660            default_rpc_url: "https://metall2.drpc.org".to_string(),
661        },
662    );
663
664    chains.insert(
665        "metal_sepolia".to_string(),
666        ChainData {
667            name: "Metal Sepolia".to_string(),
668            chain_id: 1740,
669            default_rpc_url: "https://testnet.rpc.metall2.com".to_string(),
670        },
671    );
672
673    chains.insert(
674        "binary".to_string(),
675        ChainData {
676            name: "Binary".to_string(),
677            chain_id: 624,
678            default_rpc_url: "https://rpc.zero.thebinaryholdings.com".to_string(),
679        },
680    );
681
682    chains.insert(
683        "binary_sepolia".to_string(),
684        ChainData {
685            name: "Binary Sepolia".to_string(),
686            chain_id: 625,
687            default_rpc_url: "https://rpc.zero.thebinaryholdings.com".to_string(),
688        },
689    );
690
691    chains.insert(
692        "orderly".to_string(),
693        ChainData {
694            name: "Orderly".to_string(),
695            chain_id: 291,
696            default_rpc_url: "https://rpc.orderly.network".to_string(),
697        },
698    );
699
700    chains.insert(
701        "orderly_sepolia".to_string(),
702        ChainData {
703            name: "Orderly Sepolia".to_string(),
704            chain_id: 4460,
705            default_rpc_url: "https://testnet-rpc.orderly.org".to_string(),
706        },
707    );
708
709    chains
710}
711
712#[cfg(test)]
713mod tests {
714    use super::*;
715    use foundry_config::fs_permissions::PathPermission;
716
717    fn config(root: &str, fs_permissions: FsPermissions) -> CheatsConfig {
718        CheatsConfig::new(
719            &Config { root: root.into(), fs_permissions, ..Default::default() },
720            Default::default(),
721            None,
722            None,
723        )
724    }
725
726    #[test]
727    fn test_allowed_paths() {
728        let root = "/my/project/root/";
729        let config = config(root, FsPermissions::new(vec![PathPermission::read_write("./")]));
730
731        assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Read).is_ok());
732        assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Write).is_ok());
733        assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Read).is_ok());
734        assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Write).is_ok());
735        assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Read).is_err());
736        assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Write).is_err());
737    }
738
739    #[test]
740    fn test_is_foundry_toml() {
741        let root = "/my/project/root/";
742        let config = config(root, FsPermissions::new(vec![PathPermission::read_write("./")]));
743
744        let f = format!("{root}foundry.toml");
745        assert!(config.is_foundry_toml(f));
746
747        let f = format!("{root}Foundry.toml");
748        assert!(config.is_foundry_toml(f));
749
750        let f = format!("{root}lib/other/foundry.toml");
751        assert!(!config.is_foundry_toml(f));
752    }
753}