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 split_quoted_args(args: &str) -> Result<Vec<String>, char> {
129 let mut parts = Vec::new();
130 let mut current = String::new();
131 let mut quote = None;
132 let mut escaped = false;
133 let mut token_started = false;
134
135 for ch in args.chars() {
136 if escaped {
137 current.push(ch);
138 escaped = false;
139 token_started = true;
140 continue;
141 }
142 if ch == '\\' {
143 escaped = true;
144 token_started = true;
145 continue;
146 }
147 if let Some(quote_ch) = quote {
148 if ch == quote_ch {
149 quote = None;
150 } else {
151 current.push(ch);
152 }
153 token_started = true;
154 continue;
155 }
156 if matches!(ch, '"' | '\'') {
157 quote = Some(ch);
158 token_started = true;
159 } else if ch.is_whitespace() {
160 if token_started {
161 parts.push(std::mem::take(&mut current));
162 token_started = false;
163 }
164 } else {
165 current.push(ch);
166 token_started = true;
167 }
168 }
169
170 if let Some(quote_ch) = quote {
171 return Err(quote_ch);
172 }
173 if escaped {
174 current.push('\\');
175 token_started = true;
176 }
177 if token_started {
178 parts.push(current);
179 }
180
181 Ok(parts)
182}
183
184pub fn foundry_toml_dirs(root: impl AsRef<Path>) -> Vec<PathBuf> {
206 walkdir::WalkDir::new(root)
207 .max_depth(1)
208 .into_iter()
209 .filter_map(Result::ok)
210 .filter(|e| e.file_type().is_dir())
211 .filter_map(|e| dunce::canonicalize(e.path()).ok())
212 .filter(|p| p.join(Config::FILE_NAME).exists())
213 .collect()
214}
215
216pub(crate) fn get_dir_remapping(dir: impl AsRef<Path>) -> Option<Remapping> {
218 let dir = dir.as_ref();
219 if let Some(dir_name) = dir.file_name().and_then(|s| s.to_str()).filter(|s| !s.is_empty()) {
220 let mut r = Remapping {
221 context: None,
222 name: format!("{dir_name}/"),
223 path: format!("{}", dir.display()),
224 };
225 if !r.path.ends_with('/') {
226 r.path.push('/')
227 }
228 Some(r)
229 } else {
230 None
231 }
232}
233
234pub(crate) fn deserialize_stringified_percent<'de, D>(deserializer: D) -> Result<u32, D::Error>
236where
237 D: Deserializer<'de>,
238{
239 let num: U256 = Numeric::deserialize(deserializer)?.into();
240 let num: u64 = num.try_into().map_err(serde::de::Error::custom)?;
241 if num <= 100 {
242 num.try_into().map_err(serde::de::Error::custom)
243 } else {
244 Err(serde::de::Error::custom("percent must be lte 100"))
245 }
246}
247
248pub(crate) fn deserialize_u64_or_max<'de, D>(deserializer: D) -> Result<u64, D::Error>
250where
251 D: Deserializer<'de>,
252{
253 #[derive(Deserialize)]
254 #[serde(untagged)]
255 enum Val {
256 Number(u64),
257 String(String),
258 }
259
260 match Val::deserialize(deserializer)? {
261 Val::Number(num) => Ok(num),
262 Val::String(s) if s.eq_ignore_ascii_case("max") => Ok(u64::MAX),
263 Val::String(s) => s.parse::<u64>().map_err(D::Error::custom),
264 }
265}
266
267pub(crate) fn deserialize_usize_or_max<'de, D>(deserializer: D) -> Result<usize, D::Error>
269where
270 D: Deserializer<'de>,
271{
272 deserialize_u64_or_max(deserializer)?.try_into().map_err(D::Error::custom)
273}
274
275pub(crate) fn serialize_usize_or_max<S>(value: &usize, serializer: S) -> Result<S::Ok, S::Error>
278where
279 S: Serializer,
280{
281 if *value == usize::MAX {
282 serializer.serialize_str("max")
283 } else if *value > i64::MAX as usize {
284 serializer.serialize_str(&value.to_string())
285 } else {
286 serializer.serialize_u64(*value as u64)
287 }
288}
289
290pub fn deserialize_u64_to_u256<'de, D>(deserializer: D) -> Result<U256, D::Error>
292where
293 D: Deserializer<'de>,
294{
295 #[derive(Deserialize)]
296 #[serde(untagged)]
297 enum NumericValue {
298 U256(U256),
299 U64(u64),
300 String(String),
301 }
302
303 match NumericValue::deserialize(deserializer)? {
304 NumericValue::U64(n) => Ok(U256::from(n)),
305 NumericValue::U256(n) => Ok(n),
306 NumericValue::String(s) => {
307 U256::from_str(&s).map_err(D::Error::custom)
309 }
310 }
311}
312
313pub fn serialize_u64_or_u256<S>(n: &U256, serializer: S) -> Result<S::Ok, S::Error>
318where
319 S: Serializer,
320{
321 if let Ok(n_i64) = i64::try_from(*n) {
325 serializer.serialize_i64(n_i64)
326 } else if let Ok(n_u64) = u64::try_from(*n) {
327 serializer.serialize_str(&n_u64.to_string())
328 } else {
329 serializer.serialize_str(&format!("{n:#x}"))
330 }
331}
332
333#[derive(Clone, Copy, Deserialize)]
335#[serde(untagged)]
336pub enum Numeric {
337 U256(U256),
339 Num(u64),
341}
342
343impl From<Numeric> for U256 {
344 fn from(n: Numeric) -> Self {
345 match n {
346 Numeric::U256(n) => n,
347 Numeric::Num(n) => Self::from(n),
348 }
349 }
350}
351
352impl FromStr for Numeric {
353 type Err = String;
354
355 fn from_str(s: &str) -> Result<Self, Self::Err> {
356 if s.starts_with("0x") {
357 U256::from_str_radix(s, 16).map(Numeric::U256).map_err(|err| err.to_string())
358 } else {
359 U256::from_str(s).map(Numeric::U256).map_err(|err| err.to_string())
360 }
361 }
362}