1use 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
18pub fn load_config() -> eyre::Result<Config> {
22 load_config_with_root(None)
23}
24
25pub 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
34pub 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
41pub 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 .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
77pub 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
97pub 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
106pub 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
125pub 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#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
159pub(crate) struct FoundryTomlDir {
160 pub canonical: PathBuf,
162 pub path: PathBuf,
164}
165
166pub(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
183pub(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
201pub(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
215pub(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
234pub(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
242pub(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
257pub 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 U256::from_str(&s).map_err(D::Error::custom)
276 }
277 }
278}
279
280pub fn serialize_u64_or_u256<S>(n: &U256, serializer: S) -> Result<S::Ok, S::Error>
285where
286 S: Serializer,
287{
288 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#[derive(Clone, Copy, Deserialize)]
302#[serde(untagged)]
303pub enum Numeric {
304 U256(U256),
306 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
331pub 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
348struct 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 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
373pub(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}