foundry_config/
utils.rs

1//! Utility functions
2
3use crate::Config;
4use alloy_primitives::U256;
5use figment::value::Value;
6use foundry_compilers::artifacts::{
7    remappings::{Remapping, RemappingError},
8    EvmVersion,
9};
10use revm_primitives::SpecId;
11use serde::{de::Error, Deserialize, Deserializer};
12use std::{
13    io,
14    path::{Path, PathBuf},
15    str::FromStr,
16};
17
18// TODO: Why do these exist separately from `Config::load`?
19
20/// Loads the config for the current project workspace.
21pub fn load_config() -> eyre::Result<Config> {
22    load_config_with_root(None)
23}
24
25/// Loads the config for the current project workspace or the provided root path.
26pub fn load_config_with_root(root: Option<&Path>) -> eyre::Result<Config> {
27    let root = match root {
28        Some(root) => root,
29        None => &find_project_root(None)?,
30    };
31    Ok(Config::load_with_root(root)?.sanitized())
32}
33
34/// Returns the path of the top-level directory of the working git tree.
35pub fn find_git_root(relative_to: &Path) -> io::Result<Option<PathBuf>> {
36    let root =
37        if relative_to.is_absolute() { relative_to } else { &dunce::canonicalize(relative_to)? };
38    Ok(root.ancestors().find(|p| p.join(".git").is_dir()).map(Path::to_path_buf))
39}
40
41/// Returns the root path to set for the project root.
42///
43/// Traverse the dir tree up and look for a `foundry.toml` file starting at the given path or cwd,
44/// but only until the root dir of the current repo so that:
45///
46/// ```text
47/// -- foundry.toml
48///
49/// -- repo
50///   |__ .git
51///   |__sub
52///      |__ [given_path | cwd]
53/// ```
54///
55/// will still detect `repo` as root.
56///
57/// Returns `repo` or `cwd` if no `foundry.toml` is found in the tree.
58///
59/// Returns an error if:
60/// - `cwd` is `Some` and is not a valid directory;
61/// - `cwd` is `None` and the [`std::env::current_dir`] call fails.
62pub fn find_project_root(cwd: Option<&Path>) -> io::Result<PathBuf> {
63    let cwd = match cwd {
64        Some(path) => path,
65        None => &std::env::current_dir()?,
66    };
67    let boundary = find_git_root(cwd)?;
68    let found = cwd
69        .ancestors()
70        // Don't look outside of the git repo if it exists.
71        .take_while(|p| if let Some(boundary) = &boundary { p.starts_with(boundary) } else { true })
72        .find(|p| p.join(Config::FILE_NAME).is_file())
73        .map(Path::to_path_buf);
74    Ok(found.or(boundary).unwrap_or_else(|| cwd.to_path_buf()))
75}
76
77/// Returns all [`Remapping`]s contained in the `remappings` str separated by newlines
78///
79/// # Example
80///
81/// ```
82/// use foundry_config::remappings_from_newline;
83/// let remappings: Result<Vec<_>, _> = remappings_from_newline(
84///     r#"
85///              file-ds-test/=lib/ds-test/
86///              file-other/=lib/other/
87///          "#,
88/// )
89/// .collect();
90/// ```
91pub fn remappings_from_newline(
92    remappings: &str,
93) -> impl Iterator<Item = Result<Remapping, RemappingError>> + '_ {
94    remappings.lines().map(|x| x.trim()).filter(|x| !x.is_empty()).map(Remapping::from_str)
95}
96
97/// Returns the remappings from the given var
98///
99/// Returns `None` if the env var is not set, otherwise all Remappings, See
100/// `remappings_from_newline`
101pub fn remappings_from_env_var(env_var: &str) -> Option<Result<Vec<Remapping>, RemappingError>> {
102    let val = std::env::var(env_var).ok()?;
103    Some(remappings_from_newline(&val).collect())
104}
105
106/// Converts the `val` into a `figment::Value::Array`
107///
108/// The values should be separated by commas, surrounding brackets are also supported `[a,b,c]`
109pub fn to_array_value(val: &str) -> Result<Value, figment::Error> {
110    let value: Value = match Value::from(val) {
111        Value::String(_, val) => val
112            .trim_start_matches('[')
113            .trim_end_matches(']')
114            .split(',')
115            .map(|s| s.to_string())
116            .collect::<Vec<_>>()
117            .into(),
118        Value::Empty(_, _) => Vec::<Value>::new().into(),
119        val @ Value::Array(_, _) => val,
120        _ => return Err(format!("Invalid value `{val}`, expected an array").into()),
121    };
122    Ok(value)
123}
124
125/// Returns a list of _unique_ paths to all folders under `root` that contain a `foundry.toml` file
126///
127/// This will also resolve symlinks
128///
129/// # Example
130///
131/// ```no_run
132/// use foundry_config::utils;
133/// let dirs = utils::foundry_toml_dirs("./lib");
134/// ```
135///
136/// for following layout this will return
137/// `["lib/dep1", "lib/dep2"]`
138///
139/// ```text
140/// lib
141/// └── dep1
142/// │   ├── foundry.toml
143/// └── dep2
144///     ├── foundry.toml
145/// ```
146pub fn foundry_toml_dirs(root: impl AsRef<Path>) -> Vec<PathBuf> {
147    walkdir::WalkDir::new(root)
148        .max_depth(1)
149        .into_iter()
150        .filter_map(Result::ok)
151        .filter(|e| e.file_type().is_dir())
152        .filter_map(|e| dunce::canonicalize(e.path()).ok())
153        .filter(|p| p.join(Config::FILE_NAME).exists())
154        .collect()
155}
156
157/// Returns a remapping for the given dir
158pub(crate) fn get_dir_remapping(dir: impl AsRef<Path>) -> Option<Remapping> {
159    let dir = dir.as_ref();
160    if let Some(dir_name) = dir.file_name().and_then(|s| s.to_str()).filter(|s| !s.is_empty()) {
161        let mut r = Remapping {
162            context: None,
163            name: format!("{dir_name}/"),
164            path: format!("{}", dir.display()),
165        };
166        if !r.path.ends_with('/') {
167            r.path.push('/')
168        }
169        Some(r)
170    } else {
171        None
172    }
173}
174
175/// Deserialize stringified percent. The value must be between 0 and 100 inclusive.
176pub(crate) fn deserialize_stringified_percent<'de, D>(deserializer: D) -> Result<u32, D::Error>
177where
178    D: Deserializer<'de>,
179{
180    let num: U256 = Numeric::deserialize(deserializer)?.into();
181    let num: u64 = num.try_into().map_err(serde::de::Error::custom)?;
182    if num <= 100 {
183        num.try_into().map_err(serde::de::Error::custom)
184    } else {
185        Err(serde::de::Error::custom("percent must be lte 100"))
186    }
187}
188
189/// Deserialize a `u64` or "max" for `u64::MAX`.
190pub(crate) fn deserialize_u64_or_max<'de, D>(deserializer: D) -> Result<u64, D::Error>
191where
192    D: Deserializer<'de>,
193{
194    #[derive(Deserialize)]
195    #[serde(untagged)]
196    enum Val {
197        Number(u64),
198        String(String),
199    }
200
201    match Val::deserialize(deserializer)? {
202        Val::Number(num) => Ok(num),
203        Val::String(s) if s.eq_ignore_ascii_case("max") => Ok(u64::MAX),
204        Val::String(s) => s.parse::<u64>().map_err(D::Error::custom),
205    }
206}
207
208/// Deserialize a `usize` or "max" for `usize::MAX`.
209pub(crate) fn deserialize_usize_or_max<'de, D>(deserializer: D) -> Result<usize, D::Error>
210where
211    D: Deserializer<'de>,
212{
213    deserialize_u64_or_max(deserializer)?.try_into().map_err(D::Error::custom)
214}
215
216/// Helper type to parse both `u64` and `U256`
217#[derive(Clone, Copy, Deserialize)]
218#[serde(untagged)]
219pub enum Numeric {
220    /// A [U256] value.
221    U256(U256),
222    /// A `u64` value.
223    Num(u64),
224}
225
226impl From<Numeric> for U256 {
227    fn from(n: Numeric) -> Self {
228        match n {
229            Numeric::U256(n) => n,
230            Numeric::Num(n) => Self::from(n),
231        }
232    }
233}
234
235impl FromStr for Numeric {
236    type Err = String;
237
238    fn from_str(s: &str) -> Result<Self, Self::Err> {
239        if s.starts_with("0x") {
240            U256::from_str_radix(s, 16).map(Numeric::U256).map_err(|err| err.to_string())
241        } else {
242            U256::from_str(s).map(Numeric::U256).map_err(|err| err.to_string())
243        }
244    }
245}
246
247/// Returns the [SpecId] derived from [EvmVersion]
248#[inline]
249pub fn evm_spec_id(evm_version: EvmVersion, odyssey: bool) -> SpecId {
250    if odyssey {
251        return SpecId::OSAKA;
252    }
253    match evm_version {
254        EvmVersion::Homestead => SpecId::HOMESTEAD,
255        EvmVersion::TangerineWhistle => SpecId::TANGERINE,
256        EvmVersion::SpuriousDragon => SpecId::SPURIOUS_DRAGON,
257        EvmVersion::Byzantium => SpecId::BYZANTIUM,
258        EvmVersion::Constantinople => SpecId::CONSTANTINOPLE,
259        EvmVersion::Petersburg => SpecId::PETERSBURG,
260        EvmVersion::Istanbul => SpecId::ISTANBUL,
261        EvmVersion::Berlin => SpecId::BERLIN,
262        EvmVersion::London => SpecId::LONDON,
263        EvmVersion::Paris => SpecId::MERGE,
264        EvmVersion::Shanghai => SpecId::SHANGHAI,
265        EvmVersion::Cancun => SpecId::CANCUN,
266        EvmVersion::Prague => SpecId::PRAGUE,
267        EvmVersion::Osaka => SpecId::OSAKA,
268    }
269}