1use crate::Config;
4use alloy_primitives::U256;
5use figment::value::Value;
6use foundry_compilers::artifacts::remappings::{Remapping, RemappingError};
7use serde::{Deserialize, Deserializer, Serializer, de::Error};
8use std::{
9 io,
10 path::{Path, PathBuf},
11 str::FromStr,
12};
13
14pub fn load_config() -> eyre::Result<Config> {
18 load_config_with_root(None)
19}
20
21pub fn load_config_with_root(root: Option<&Path>) -> eyre::Result<Config> {
23 let root = match root {
24 Some(root) => root,
25 None => &find_project_root(None)?,
26 };
27 Ok(Config::load_with_root(root)?.sanitized())
28}
29
30pub fn find_git_root(relative_to: &Path) -> io::Result<Option<PathBuf>> {
32 let root =
33 if relative_to.is_absolute() { relative_to } else { &dunce::canonicalize(relative_to)? };
34 Ok(root.ancestors().find(|p| p.join(".git").exists()).map(Path::to_path_buf))
35}
36
37pub fn find_project_root(cwd: Option<&Path>) -> io::Result<PathBuf> {
59 let cwd = match cwd {
60 Some(path) => path,
61 None => &std::env::current_dir()?,
62 };
63 let boundary = find_git_root(cwd)?;
64 let found = cwd
65 .ancestors()
66 .take_while(|p| if let Some(boundary) = &boundary { p.starts_with(boundary) } else { true })
68 .find(|p| p.join(Config::FILE_NAME).is_file())
69 .map(Path::to_path_buf);
70 Ok(found.or(boundary).unwrap_or_else(|| cwd.to_path_buf()))
71}
72
73pub fn remappings_from_newline(
88 remappings: &str,
89) -> impl Iterator<Item = Result<Remapping, RemappingError>> + '_ {
90 remappings.lines().map(|x| x.trim()).filter(|x| !x.is_empty()).map(Remapping::from_str)
91}
92
93pub fn remappings_from_env_var(env_var: &str) -> Option<Result<Vec<Remapping>, RemappingError>> {
98 let val = std::env::var(env_var).ok()?;
99 Some(remappings_from_newline(&val).collect())
100}
101
102pub fn to_array_value(val: &str) -> Result<Value, figment::Error> {
106 let value: Value = match Value::from(val) {
107 Value::String(_, val) => val
108 .trim_start_matches('[')
109 .trim_end_matches(']')
110 .split(',')
111 .map(|s| s.to_string())
112 .collect::<Vec<_>>()
113 .into(),
114 Value::Empty(_, _) => Vec::<Value>::new().into(),
115 val @ Value::Array(_, _) => val,
116 _ => return Err(format!("Invalid value `{val}`, expected an array").into()),
117 };
118 Ok(value)
119}
120
121pub fn foundry_toml_dirs(root: impl AsRef<Path>) -> Vec<PathBuf> {
143 walkdir::WalkDir::new(root)
144 .max_depth(1)
145 .into_iter()
146 .filter_map(Result::ok)
147 .filter(|entry| entry.file_type().is_dir())
148 .filter_map(|entry| dunce::canonicalize(entry.path()).ok())
149 .filter(|path| path.join(Config::FILE_NAME).exists())
150 .collect()
151}
152
153#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
155pub(crate) struct FoundryTomlDir {
156 pub canonical: PathBuf,
158 pub path: PathBuf,
160}
161
162pub(crate) fn foundry_toml_dir_entries(root: impl AsRef<Path>) -> Vec<FoundryTomlDir> {
164 let mut dirs = walkdir::WalkDir::new(root)
165 .max_depth(1)
166 .into_iter()
167 .filter_map(Result::ok)
168 .filter_map(|entry| {
169 let canonical = dunce::canonicalize(entry.path()).ok()?;
170 (canonical.is_dir() && canonical.join(Config::FILE_NAME).exists())
171 .then(|| FoundryTomlDir { path: entry.path().to_path_buf(), canonical })
172 })
173 .collect::<Vec<_>>();
174 dirs.sort_unstable_by(|a, b| a.canonical.cmp(&b.canonical).then_with(|| a.path.cmp(&b.path)));
175 dirs.dedup();
176 dirs
177}
178
179pub(crate) fn get_dir_remapping(dir: impl AsRef<Path>) -> Option<Remapping> {
181 let dir = dir.as_ref();
182 if let Some(dir_name) = dir.file_name().and_then(|s| s.to_str()).filter(|s| !s.is_empty()) {
183 let mut r = Remapping {
184 context: None,
185 name: format!("{dir_name}/"),
186 path: format!("{}", dir.display()),
187 };
188 if !r.path.ends_with('/') {
189 r.path.push('/')
190 }
191 Some(r)
192 } else {
193 None
194 }
195}
196
197pub(crate) fn deserialize_stringified_percent<'de, D>(deserializer: D) -> Result<u32, D::Error>
199where
200 D: Deserializer<'de>,
201{
202 let num: U256 = Numeric::deserialize(deserializer)?.into();
203 let num: u64 = num.try_into().map_err(serde::de::Error::custom)?;
204 if num <= 100 {
205 num.try_into().map_err(serde::de::Error::custom)
206 } else {
207 Err(serde::de::Error::custom("percent must be lte 100"))
208 }
209}
210
211pub(crate) fn deserialize_u64_or_max<'de, D>(deserializer: D) -> Result<u64, D::Error>
213where
214 D: Deserializer<'de>,
215{
216 #[derive(Deserialize)]
217 #[serde(untagged)]
218 enum Val {
219 Number(u64),
220 String(String),
221 }
222
223 match Val::deserialize(deserializer)? {
224 Val::Number(num) => Ok(num),
225 Val::String(s) if s.eq_ignore_ascii_case("max") => Ok(u64::MAX),
226 Val::String(s) => s.parse::<u64>().map_err(D::Error::custom),
227 }
228}
229
230pub(crate) fn deserialize_usize_or_max<'de, D>(deserializer: D) -> Result<usize, D::Error>
232where
233 D: Deserializer<'de>,
234{
235 deserialize_u64_or_max(deserializer)?.try_into().map_err(D::Error::custom)
236}
237
238pub(crate) fn serialize_usize_or_max<S>(value: &usize, serializer: S) -> Result<S::Ok, S::Error>
241where
242 S: Serializer,
243{
244 if *value == usize::MAX {
245 serializer.serialize_str("max")
246 } else if *value > i64::MAX as usize {
247 serializer.serialize_str(&value.to_string())
248 } else {
249 serializer.serialize_u64(*value as u64)
250 }
251}
252
253pub fn deserialize_u64_to_u256<'de, D>(deserializer: D) -> Result<U256, D::Error>
255where
256 D: Deserializer<'de>,
257{
258 #[derive(Deserialize)]
259 #[serde(untagged)]
260 enum NumericValue {
261 U256(U256),
262 U64(u64),
263 String(String),
264 }
265
266 match NumericValue::deserialize(deserializer)? {
267 NumericValue::U64(n) => Ok(U256::from(n)),
268 NumericValue::U256(n) => Ok(n),
269 NumericValue::String(s) => {
270 U256::from_str(&s).map_err(D::Error::custom)
272 }
273 }
274}
275
276pub fn serialize_u64_or_u256<S>(n: &U256, serializer: S) -> Result<S::Ok, S::Error>
281where
282 S: Serializer,
283{
284 if let Ok(n_i64) = i64::try_from(*n) {
288 serializer.serialize_i64(n_i64)
289 } else if let Ok(n_u64) = u64::try_from(*n) {
290 serializer.serialize_str(&n_u64.to_string())
291 } else {
292 serializer.serialize_str(&format!("{n:#x}"))
293 }
294}
295
296#[derive(Clone, Copy, Deserialize)]
298#[serde(untagged)]
299pub enum Numeric {
300 U256(U256),
302 Num(u64),
304}
305
306impl From<Numeric> for U256 {
307 fn from(n: Numeric) -> Self {
308 match n {
309 Numeric::U256(n) => n,
310 Numeric::Num(n) => Self::from(n),
311 }
312 }
313}
314
315impl FromStr for Numeric {
316 type Err = String;
317
318 fn from_str(s: &str) -> Result<Self, Self::Err> {
319 if s.starts_with("0x") {
320 U256::from_str_radix(s, 16).map(Numeric::U256).map_err(|err| err.to_string())
321 } else {
322 U256::from_str(s).map(Numeric::U256).map_err(|err| err.to_string())
323 }
324 }
325}
326
327#[cfg(all(test, unix))]
328mod tests {
329 use super::{FoundryTomlDir, foundry_toml_dir_entries, foundry_toml_dirs};
330 use std::{fs, os::unix::fs::symlink};
331 use tempfile::tempdir;
332
333 #[test]
334 fn finds_physical_foundry_toml_dirs_without_recursing() {
335 let temp = tempdir().unwrap();
336 let root = temp.path().join("lib");
337 let dependency = root.join("dependency");
338 let nested = dependency.join("nested");
339 fs::create_dir_all(&nested).unwrap();
340 fs::write(dependency.join("foundry.toml"), "").unwrap();
341 fs::write(nested.join("foundry.toml"), "").unwrap();
342
343 assert_eq!(foundry_toml_dirs(&root), vec![dunce::canonicalize(dependency).unwrap()]);
344 }
345
346 #[test]
347 fn preserves_sorted_aliases_for_the_same_canonical_dependency() {
348 let temp = tempdir().unwrap();
349 let root = temp.path().join("lib");
350 let dependency = temp.path().join("dependency");
351 fs::create_dir_all(&root).unwrap();
352 fs::create_dir_all(&dependency).unwrap();
353 fs::write(dependency.join("foundry.toml"), "").unwrap();
354 symlink(&dependency, root.join("z-alias")).unwrap();
355 symlink(&dependency, root.join("a-alias")).unwrap();
356
357 let canonical = dunce::canonicalize(dependency).unwrap();
358 assert_eq!(
359 foundry_toml_dir_entries(&root),
360 vec![
361 FoundryTomlDir { path: root.join("a-alias"), canonical: canonical.clone() },
362 FoundryTomlDir { path: root.join("z-alias"), canonical },
363 ]
364 );
365 }
366
367 #[test]
368 fn sorts_distinct_dependencies_by_canonical_path() {
369 let temp = tempdir().unwrap();
370 let root = temp.path().join("lib");
371 let dependency_a = temp.path().join("dependency-a");
372 let dependency_z = temp.path().join("dependency-z");
373 fs::create_dir_all(&root).unwrap();
374 for dependency in [&dependency_z, &dependency_a] {
375 fs::create_dir_all(dependency).unwrap();
376 fs::write(dependency.join("foundry.toml"), "").unwrap();
377 }
378 symlink(&dependency_z, root.join("a-alias")).unwrap();
379 symlink(&dependency_a, root.join("z-alias")).unwrap();
380
381 let canonical = foundry_toml_dir_entries(&root)
382 .into_iter()
383 .map(|entry| entry.canonical)
384 .collect::<Vec<_>>();
385 assert_eq!(
386 canonical,
387 vec![
388 dunce::canonicalize(dependency_a).unwrap(),
389 dunce::canonicalize(dependency_z).unwrap(),
390 ]
391 );
392 }
393
394 #[test]
395 fn ignores_broken_and_cyclic_symlinks() {
396 let temp = tempdir().unwrap();
397 let root = temp.path().join("lib");
398 fs::create_dir_all(&root).unwrap();
399 symlink("missing", root.join("broken")).unwrap();
400 symlink("cycle-b", root.join("cycle-a")).unwrap();
401 symlink("cycle-a", root.join("cycle-b")).unwrap();
402
403 assert!(foundry_toml_dir_entries(&root).is_empty());
404 }
405}