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