Skip to main content

foundry_config/
utils.rs

1//! Utility functions
2
3use crate::Config;
4use alloy_primitives::U256;
5use figment::value::Value;
6use foundry_compilers::artifacts::remappings::{Remapping, RemappingError};
7use serde::{
8    Deserialize, Deserializer, Serializer,
9    de::{DeserializeOwned, Error, IgnoredAny, MapAccess, Visitor},
10};
11use std::{
12    fmt, fs,
13    io::{self, BufReader},
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").exists()).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.trim().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(|entry| entry.file_type().is_dir())
152        .filter_map(|entry| dunce::canonicalize(entry.path()).ok())
153        .filter(|path| path.join(Config::FILE_NAME).exists())
154        .collect()
155}
156
157/// A depth-one dependency path and the canonical directory it resolves to.
158#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
159pub(crate) struct FoundryTomlDir {
160    /// The canonical directory used to identify the dependency config.
161    pub canonical: PathBuf,
162    /// The lexical path under the configured dependency directory.
163    pub path: PathBuf,
164}
165
166/// Returns depth-one dependency entries that resolve to directories containing `foundry.toml`.
167pub(crate) fn foundry_toml_dir_entries(root: impl AsRef<Path>) -> Vec<FoundryTomlDir> {
168    let mut dirs = walkdir::WalkDir::new(root)
169        .max_depth(1)
170        .into_iter()
171        .filter_map(Result::ok)
172        .filter_map(|entry| {
173            let canonical = dunce::canonicalize(entry.path()).ok()?;
174            (canonical.is_dir() && canonical.join(Config::FILE_NAME).exists())
175                .then(|| FoundryTomlDir { path: entry.path().to_path_buf(), canonical })
176        })
177        .collect::<Vec<_>>();
178    dirs.sort_unstable_by(|a, b| a.canonical.cmp(&b.canonical).then_with(|| a.path.cmp(&b.path)));
179    dirs.dedup();
180    dirs
181}
182
183/// Returns a remapping for the given dir
184pub(crate) fn get_dir_remapping(dir: impl AsRef<Path>) -> Option<Remapping> {
185    let dir = dir.as_ref();
186    if let Some(dir_name) = dir.file_name().and_then(|s| s.to_str()).filter(|s| !s.is_empty()) {
187        let mut r = Remapping {
188            context: None,
189            name: format!("{dir_name}/"),
190            path: format!("{}", dir.display()),
191        };
192        if !r.path.ends_with('/') {
193            r.path.push('/')
194        }
195        Some(r)
196    } else {
197        None
198    }
199}
200
201/// Deserialize stringified percent. The value must be between 0 and 100 inclusive.
202pub(crate) fn deserialize_stringified_percent<'de, D>(deserializer: D) -> Result<u32, D::Error>
203where
204    D: Deserializer<'de>,
205{
206    let num: U256 = Numeric::deserialize(deserializer)?.into();
207    let num: u64 = num.try_into().map_err(serde::de::Error::custom)?;
208    if num <= 100 {
209        num.try_into().map_err(serde::de::Error::custom)
210    } else {
211        Err(serde::de::Error::custom("percent must be lte 100"))
212    }
213}
214
215/// Deserialize a `u64` or "max" for `u64::MAX`.
216pub(crate) fn deserialize_u64_or_max<'de, D>(deserializer: D) -> Result<u64, D::Error>
217where
218    D: Deserializer<'de>,
219{
220    #[derive(Deserialize)]
221    #[serde(untagged)]
222    enum Val {
223        Number(u64),
224        String(String),
225    }
226
227    match Val::deserialize(deserializer)? {
228        Val::Number(num) => Ok(num),
229        Val::String(s) if s.eq_ignore_ascii_case("max") => Ok(u64::MAX),
230        Val::String(s) => s.parse::<u64>().map_err(D::Error::custom),
231    }
232}
233
234/// Deserialize a `usize` or "max" for `usize::MAX`.
235pub(crate) fn deserialize_usize_or_max<'de, D>(deserializer: D) -> Result<usize, D::Error>
236where
237    D: Deserializer<'de>,
238{
239    deserialize_u64_or_max(deserializer)?.try_into().map_err(D::Error::custom)
240}
241
242/// Serialize a `usize` as `"max"` if it equals `usize::MAX`, as a string if it exceeds
243/// `i64::MAX` (TOML integer limit), or as a plain number otherwise.
244pub(crate) fn serialize_usize_or_max<S>(value: &usize, serializer: S) -> Result<S::Ok, S::Error>
245where
246    S: Serializer,
247{
248    if *value == usize::MAX {
249        serializer.serialize_str("max")
250    } else if *value > i64::MAX as usize {
251        serializer.serialize_str(&value.to_string())
252    } else {
253        serializer.serialize_u64(*value as u64)
254    }
255}
256
257/// Deserialize into `U256` from either a `u64`, a `U256` hex string, or a decimal string.
258pub fn deserialize_u64_to_u256<'de, D>(deserializer: D) -> Result<U256, D::Error>
259where
260    D: Deserializer<'de>,
261{
262    #[derive(Deserialize)]
263    #[serde(untagged)]
264    enum NumericValue {
265        U256(U256),
266        U64(u64),
267        String(String),
268    }
269
270    match NumericValue::deserialize(deserializer)? {
271        NumericValue::U64(n) => Ok(U256::from(n)),
272        NumericValue::U256(n) => Ok(n),
273        NumericValue::String(s) => {
274            // Handle decimal strings (e.g., "18446744073709551615")
275            U256::from_str(&s).map_err(D::Error::custom)
276        }
277    }
278}
279
280/// Serialize `U256` as `u64` if it fits, otherwise as a hex string.
281/// If the number fits into a i64, serialize it as number without quotation marks.
282/// If the number fits into a u64, serialize it as a stringified number with quotation marks.
283/// Otherwise, serialize it as a hex string with quotation marks.
284pub fn serialize_u64_or_u256<S>(n: &U256, serializer: S) -> Result<S::Ok, S::Error>
285where
286    S: Serializer,
287{
288    // The TOML specification handles integers as i64 so the number representation is limited to
289    // i64. If the number is larger than `i64::MAX` and up to `u64::MAX`, we serialize it as a
290    // string to avoid losing precision.
291    if let Ok(n_i64) = i64::try_from(*n) {
292        serializer.serialize_i64(n_i64)
293    } else if let Ok(n_u64) = u64::try_from(*n) {
294        serializer.serialize_str(&n_u64.to_string())
295    } else {
296        serializer.serialize_str(&format!("{n:#x}"))
297    }
298}
299
300/// Helper type to parse both `u64` and `U256`
301#[derive(Clone, Copy, Deserialize)]
302#[serde(untagged)]
303pub enum Numeric {
304    /// A [U256] value.
305    U256(U256),
306    /// A `u64` value.
307    Num(u64),
308}
309
310impl From<Numeric> for U256 {
311    fn from(n: Numeric) -> Self {
312        match n {
313            Numeric::U256(n) => n,
314            Numeric::Num(n) => Self::from(n),
315        }
316    }
317}
318
319impl FromStr for Numeric {
320    type Err = String;
321
322    fn from_str(s: &str) -> Result<Self, Self::Err> {
323        if s.starts_with("0x") {
324            U256::from_str_radix(s, 16).map(Numeric::U256).map_err(|err| err.to_string())
325        } else {
326            U256::from_str(s).map(Numeric::U256).map_err(|err| err.to_string())
327        }
328    }
329}
330
331/// Reads the compiler `input` of the build info at `path`, or `None` if the build info leaves it
332/// out, as build infos do unless `build_info` is set. A JSON object with neither an `id` nor an
333/// `input` is not a build info and fails.
334///
335/// The read stops after the input, which build infos write before the much larger output.
336pub fn read_build_info_input<T: DeserializeOwned>(path: &Path) -> eyre::Result<Option<T>> {
337    let mut input = None;
338    let reader = BufReader::new(fs::File::open(path)?);
339    let read =
340        serde_json::Deserializer::from_reader(reader).deserialize_map(BuildInfoInput(&mut input));
341    match (input, read) {
342        (Some(input), _) => Ok(Some(input)),
343        (None, Ok(())) => Ok(None),
344        (None, Err(error)) => Err(error.into()),
345    }
346}
347
348/// Reads the `input` of a build info into `input`.
349struct BuildInfoInput<'a, T>(&'a mut Option<T>);
350
351impl<'de, T: Deserialize<'de>> Visitor<'de> for BuildInfoInput<'_, T> {
352    type Value = ();
353
354    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
355        formatter.write_str("a build info")
356    }
357
358    fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<(), A::Error> {
359        let mut id = false;
360        while let Some(key) = map.next_key::<String>()? {
361            if key == "input" {
362                *self.0 = Some(map.next_value()?);
363                // An error is the only way to stop the deserializer before the end of the map.
364                return Err(A::Error::custom("stopped after the input"));
365            }
366            id |= key == "id";
367            map.next_value::<IgnoredAny>()?;
368        }
369        if id { Ok(()) } else { Err(A::Error::custom("not a build info")) }
370    }
371}
372
373/// A value that is there, deserialized without reading it. [`read_build_info_input`] stops after
374/// the input, so `read_build_info_input::<Present>` only checks that a build info has one.
375pub(crate) struct Present;
376
377impl<'de> Deserialize<'de> for Present {
378    fn deserialize<D: Deserializer<'de>>(_deserializer: D) -> Result<Self, D::Error> {
379        Ok(Self)
380    }
381}
382
383#[cfg(all(test, unix))]
384mod tests {
385    use super::{FoundryTomlDir, foundry_toml_dir_entries, foundry_toml_dirs};
386    use std::{fs, os::unix::fs::symlink};
387    use tempfile::tempdir;
388
389    #[test]
390    fn finds_physical_foundry_toml_dirs_without_recursing() {
391        let temp = tempdir().unwrap();
392        let root = temp.path().join("lib");
393        let dependency = root.join("dependency");
394        let nested = dependency.join("nested");
395        fs::create_dir_all(&nested).unwrap();
396        fs::write(dependency.join("foundry.toml"), "").unwrap();
397        fs::write(nested.join("foundry.toml"), "").unwrap();
398
399        assert_eq!(foundry_toml_dirs(&root), vec![dunce::canonicalize(dependency).unwrap()]);
400    }
401
402    #[test]
403    fn preserves_sorted_aliases_for_the_same_canonical_dependency() {
404        let temp = tempdir().unwrap();
405        let root = temp.path().join("lib");
406        let dependency = temp.path().join("dependency");
407        fs::create_dir_all(&root).unwrap();
408        fs::create_dir_all(&dependency).unwrap();
409        fs::write(dependency.join("foundry.toml"), "").unwrap();
410        symlink(&dependency, root.join("z-alias")).unwrap();
411        symlink(&dependency, root.join("a-alias")).unwrap();
412
413        let canonical = dunce::canonicalize(dependency).unwrap();
414        assert_eq!(
415            foundry_toml_dir_entries(&root),
416            vec![
417                FoundryTomlDir { path: root.join("a-alias"), canonical: canonical.clone() },
418                FoundryTomlDir { path: root.join("z-alias"), canonical },
419            ]
420        );
421    }
422
423    #[test]
424    fn sorts_distinct_dependencies_by_canonical_path() {
425        let temp = tempdir().unwrap();
426        let root = temp.path().join("lib");
427        let dependency_a = temp.path().join("dependency-a");
428        let dependency_z = temp.path().join("dependency-z");
429        fs::create_dir_all(&root).unwrap();
430        for dependency in [&dependency_z, &dependency_a] {
431            fs::create_dir_all(dependency).unwrap();
432            fs::write(dependency.join("foundry.toml"), "").unwrap();
433        }
434        symlink(&dependency_z, root.join("a-alias")).unwrap();
435        symlink(&dependency_a, root.join("z-alias")).unwrap();
436
437        let canonical = foundry_toml_dir_entries(&root)
438            .into_iter()
439            .map(|entry| entry.canonical)
440            .collect::<Vec<_>>();
441        assert_eq!(
442            canonical,
443            vec![
444                dunce::canonicalize(dependency_a).unwrap(),
445                dunce::canonicalize(dependency_z).unwrap(),
446            ]
447        );
448    }
449
450    #[test]
451    fn ignores_broken_and_cyclic_symlinks() {
452        let temp = tempdir().unwrap();
453        let root = temp.path().join("lib");
454        fs::create_dir_all(&root).unwrap();
455        symlink("missing", root.join("broken")).unwrap();
456        symlink("cycle-b", root.join("cycle-a")).unwrap();
457        symlink("cycle-a", root.join("cycle-b")).unwrap();
458
459        assert!(foundry_toml_dir_entries(&root).is_empty());
460    }
461}