foundry_compilers_core/utils/
mod.rs1use crate::error::{SolcError, SolcIoError};
4use alloy_primitives::{hex, keccak256};
5use cfg_if::cfg_if;
6use semver::{Version, VersionReq};
7use serde::{de::DeserializeOwned, Serialize};
8use std::{
9 fs,
10 io::Write,
11 ops::Range,
12 path::{Component, Path, PathBuf},
13 sync::LazyLock as Lazy,
14};
15
16#[cfg(feature = "regex")]
17mod re;
18#[cfg(feature = "regex")]
19pub use re::*;
20
21#[cfg(feature = "walkdir")]
22mod wd;
23#[cfg(feature = "walkdir")]
24pub use wd::*;
25
26pub const SOLC_EXTENSIONS: &[&str] = &["sol", "yul"];
28
29pub const BYZANTIUM_SOLC: Version = Version::new(0, 4, 21);
32
33pub const CONSTANTINOPLE_SOLC: Version = Version::new(0, 4, 22);
36
37pub const PETERSBURG_SOLC: Version = Version::new(0, 5, 5);
40
41pub const ISTANBUL_SOLC: Version = Version::new(0, 5, 14);
44
45pub const BERLIN_SOLC: Version = Version::new(0, 8, 5);
48
49pub const LONDON_SOLC: Version = Version::new(0, 8, 7);
52
53pub const PARIS_SOLC: Version = Version::new(0, 8, 18);
56
57pub const SHANGHAI_SOLC: Version = Version::new(0, 8, 20);
60
61pub const CANCUN_SOLC: Version = Version::new(0, 8, 24);
64
65pub const PRAGUE_SOLC: Version = Version::new(0, 8, 27);
68
69pub static SUPPORTS_BASE_PATH: Lazy<VersionReq> =
71 Lazy::new(|| VersionReq::parse(">=0.6.9").unwrap());
72
73pub static SUPPORTS_INCLUDE_PATH: Lazy<VersionReq> =
75 Lazy::new(|| VersionReq::parse(">=0.8.8").unwrap());
76
77pub fn range_by_offset(range: &Range<usize>, offset: isize) -> Range<usize> {
79 Range {
80 start: offset.saturating_add(range.start as isize) as usize,
81 end: offset.saturating_add(range.end as isize) as usize,
82 }
83}
84
85pub fn source_name<'a>(source: &'a Path, root: &Path) -> &'a Path {
89 strip_prefix(source, root)
90}
91
92pub fn strip_prefix<'a>(source: &'a Path, root: &Path) -> &'a Path {
94 source.strip_prefix(root).unwrap_or(source)
95}
96
97pub fn strip_prefix_owned(source: PathBuf, root: &Path) -> PathBuf {
99 source.strip_prefix(root).map(Path::to_path_buf).unwrap_or(source)
100}
101
102pub fn is_local_source_name(libs: &[impl AsRef<Path>], source: impl AsRef<Path>) -> bool {
104 resolve_library(libs, source.as_ref()).is_none()
105}
106
107pub fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf, SolcIoError> {
111 let path = path.as_ref();
112 let res = dunce::canonicalize(path);
113 #[cfg(windows)]
114 let res = res.map(|p| {
115 use path_slash::PathBufExt;
116 PathBuf::from(p.to_slash_lossy().as_ref())
117 });
118 res.map_err(|err| SolcIoError::new(err, path))
119}
120
121pub fn normalize_solidity_import_path(
131 directory: &Path,
132 import_path: &Path,
133) -> Result<PathBuf, SolcIoError> {
134 let original = directory.join(import_path);
135 let cleaned = clean_solidity_path(&original);
136
137 let normalized = dunce::simplified(&cleaned);
139 #[cfg(windows)]
140 let normalized = {
141 use path_slash::PathExt;
142 PathBuf::from(normalized.to_slash_lossy().as_ref())
143 };
144 #[cfg(not(windows))]
145 let normalized = PathBuf::from(normalized);
146
147 let _ = normalized.metadata().map_err(|err| SolcIoError::new(err, original))?;
149 Ok(normalized)
150}
151
152fn clean_solidity_path(original_path: &Path) -> PathBuf {
182 let mut new_path = Vec::new();
183
184 for component in original_path.components() {
185 match component {
186 Component::Prefix(..) | Component::RootDir | Component::Normal(..) => {
187 new_path.push(component);
188 }
189 Component::CurDir => {}
190 Component::ParentDir => {
191 if let Some(Component::Normal(..)) = new_path.last() {
192 new_path.pop();
193 } else {
194 new_path.push(component);
195 }
196 }
197 }
198 }
199
200 new_path.iter().collect()
201}
202
203pub fn canonicalized(path: impl Into<PathBuf>) -> PathBuf {
213 let path = path.into();
214 canonicalize(&path).unwrap_or(path)
215}
216
217pub fn resolve_library(libs: &[impl AsRef<Path>], source: impl AsRef<Path>) -> Option<PathBuf> {
221 let source = source.as_ref();
222 let comp = source.components().next()?;
223 match comp {
224 Component::Normal(first_dir) => {
225 for lib in libs {
228 let lib = lib.as_ref();
229 let contract = lib.join(source);
230 if contract.exists() {
231 return Some(contract);
233 }
234 let contract = lib
236 .join(first_dir)
237 .join("src")
238 .join(source.strip_prefix(first_dir).expect("is first component"));
239 if contract.exists() {
240 return Some(contract);
241 }
242 }
243 None
244 }
245 Component::RootDir => Some(source.into()),
246 _ => None,
247 }
248}
249
250pub fn resolve_absolute_library(
267 root: &Path,
268 cwd: &Path,
269 import: &Path,
270) -> Option<(PathBuf, PathBuf)> {
271 let mut parent = cwd.parent()?;
272 while parent != root {
273 if let Ok(import) = normalize_solidity_import_path(parent, import) {
274 return Some((parent.to_path_buf(), import));
275 }
276 parent = parent.parent()?;
277 }
278 None
279}
280
281pub fn library_fully_qualified_placeholder(name: &str) -> String {
286 name.chars().chain(std::iter::repeat('_')).take(36).collect()
287}
288
289pub fn library_hash_placeholder(name: impl AsRef<[u8]>) -> String {
291 let mut s = String::with_capacity(34 + 2);
292 s.push('$');
293 s.push_str(hex::Buffer::<17, false>::new().format(&library_hash(name)));
294 s.push('$');
295 s
296}
297
298pub fn library_hash(name: impl AsRef<[u8]>) -> [u8; 17] {
304 let hash = keccak256(name);
305 hash[..17].try_into().unwrap()
306}
307
308pub fn common_ancestor_all<I, P>(paths: I) -> Option<PathBuf>
323where
324 I: IntoIterator<Item = P>,
325 P: AsRef<Path>,
326{
327 let mut iter = paths.into_iter();
328 let mut ret = iter.next()?.as_ref().to_path_buf();
329 for path in iter {
330 if let Some(r) = common_ancestor(&ret, path.as_ref()) {
331 ret = r;
332 } else {
333 return None;
334 }
335 }
336 Some(ret)
337}
338
339pub fn common_ancestor(a: &Path, b: &Path) -> Option<PathBuf> {
353 let a = a.components();
354 let b = b.components();
355 let mut ret = PathBuf::new();
356 let mut found = false;
357 for (c1, c2) in a.zip(b) {
358 if c1 == c2 {
359 ret.push(c1);
360 found = true;
361 } else {
362 break;
363 }
364 }
365 if found {
366 Some(ret)
367 } else {
368 None
369 }
370}
371
372pub fn find_fave_or_alt_path(root: &Path, fave: &str, alt: &str) -> PathBuf {
377 let p = root.join(fave);
378 if !p.exists() {
379 let alt = root.join(alt);
380 if alt.exists() {
381 return alt;
382 }
383 }
384 p
385}
386
387cfg_if! {
388 if #[cfg(any(feature = "async", feature = "svm-solc"))] {
389 use tokio::runtime::{Handle, Runtime};
390
391 #[derive(Debug)]
392 pub enum RuntimeOrHandle {
393 Runtime(Runtime),
394 Handle(Handle),
395 }
396
397 impl Default for RuntimeOrHandle {
398 fn default() -> Self {
399 Self::new()
400 }
401 }
402
403 impl RuntimeOrHandle {
404 pub fn new() -> Self {
405 match Handle::try_current() {
406 Ok(handle) => Self::Handle(handle),
407 Err(_) => Self::Runtime(Runtime::new().expect("Failed to start runtime")),
408 }
409 }
410
411 pub fn block_on<F: std::future::Future>(&self, f: F) -> F::Output {
412 match &self {
413 Self::Runtime(runtime) => runtime.block_on(f),
414 Self::Handle(handle) => tokio::task::block_in_place(|| handle.block_on(f)),
415 }
416 }
417 }
418 }
419}
420
421#[cfg(any(test, feature = "project-util", feature = "test-utils"))]
423pub fn tempdir(name: &str) -> Result<tempfile::TempDir, SolcIoError> {
424 tempfile::Builder::new().prefix(name).tempdir().map_err(|err| SolcIoError::new(err, name))
425}
426
427pub fn read_json_file<T: DeserializeOwned>(path: &Path) -> Result<T, SolcError> {
429 let s = fs::read_to_string(path).map_err(|err| SolcError::io(err, path))?;
431 serde_json::from_str(&s).map_err(Into::into)
432}
433
434pub fn write_json_file<T: Serialize>(
436 value: &T,
437 path: &Path,
438 capacity: usize,
439) -> Result<(), SolcError> {
440 let file = fs::File::create(path).map_err(|err| SolcError::io(err, path))?;
441 let mut writer = std::io::BufWriter::with_capacity(capacity, file);
442 serde_json::to_writer(&mut writer, value)?;
443 writer.flush().map_err(|e| SolcError::io(e, path))
444}
445
446pub fn create_parent_dir_all(file: &Path) -> Result<(), SolcError> {
450 if let Some(parent) = file.parent() {
451 fs::create_dir_all(parent).map_err(|err| {
452 SolcError::msg(format!(
453 "Failed to create artifact parent folder \"{}\": {}",
454 parent.display(),
455 err
456 ))
457 })?;
458 }
459 Ok(())
460}
461
462#[cfg(any(test, feature = "test-utils"))]
463pub fn touch(path: &std::path::Path) -> std::io::Result<()> {
465 match std::fs::OpenOptions::new().create(true).write(true).truncate(false).open(path) {
466 Ok(_) => Ok(()),
467 Err(e) => Err(e),
468 }
469}
470
471#[cfg(any(test, feature = "test-utils"))]
472pub fn mkdir_or_touch(tmp: &std::path::Path, paths: &[&str]) {
473 for path in paths {
474 if let Some(parent) = Path::new(path).parent() {
475 std::fs::create_dir_all(tmp.join(parent)).unwrap();
476 }
477 if path.ends_with(".sol") {
478 let path = tmp.join(path);
479 touch(&path).unwrap();
480 } else {
481 let path: PathBuf = tmp.join(path);
482 std::fs::create_dir_all(path).unwrap();
483 }
484 }
485}
486
487#[cfg(test)]
488mod tests {
489 pub use super::*;
490 pub use std::fs::{create_dir_all, File};
491
492 #[test]
493 fn can_create_parent_dirs_with_ext() {
494 let tmp_dir = tempdir("out").unwrap();
495 let path = tmp_dir.path().join("IsolationModeMagic.sol/IsolationModeMagic.json");
496 create_parent_dir_all(&path).unwrap();
497 assert!(path.parent().unwrap().is_dir());
498 }
499
500 #[test]
501 fn can_create_parent_dirs_versioned() {
502 let tmp_dir = tempdir("out").unwrap();
503 let path = tmp_dir.path().join("IVersioned.sol/IVersioned.0.8.16.json");
504 create_parent_dir_all(&path).unwrap();
505 assert!(path.parent().unwrap().is_dir());
506 let path = tmp_dir.path().join("IVersioned.sol/IVersioned.json");
507 create_parent_dir_all(&path).unwrap();
508 assert!(path.parent().unwrap().is_dir());
509 }
510
511 #[test]
512 fn can_determine_local_paths() {
513 assert!(is_local_source_name(&[""], "./local/contract.sol"));
514 assert!(is_local_source_name(&[""], "../local/contract.sol"));
515 assert!(!is_local_source_name(&[""], "/ds-test/test.sol"));
516
517 let tmp_dir = tempdir("contracts").unwrap();
518 let dir = tmp_dir.path().join("ds-test");
519 create_dir_all(&dir).unwrap();
520 File::create(dir.join("test.sol")).unwrap();
521
522 assert!(!is_local_source_name(&[tmp_dir.path()], "ds-test/test.sol"));
523 }
524
525 #[test]
526 fn can_normalize_solidity_import_path() {
527 let dir = tempfile::tempdir().unwrap();
528 let dir_path = dir.path();
529
530 fs::create_dir_all(dir_path.join("src/common")).unwrap();
539 fs::write(dir_path.join("src/Token.sol"), "").unwrap();
540 fs::write(dir_path.join("src/common/Burnable.sol"), "").unwrap();
541
542 let cwd = dir_path.join("src");
544
545 assert_eq!(
546 normalize_solidity_import_path(&cwd, "./common/Burnable.sol".as_ref()).unwrap(),
547 dir_path.join("src/common/Burnable.sol"),
548 );
549
550 assert!(normalize_solidity_import_path(&cwd, "./common/Pausable.sol".as_ref()).is_err());
551 }
552
553 #[test]
556 #[cfg(unix)]
557 fn can_normalize_solidity_import_path_symlink() {
558 let dir = tempfile::tempdir().unwrap();
559 let dir_path = dir.path();
560
561 fs::create_dir_all(dir_path.join("project/src")).unwrap();
573 fs::write(dir_path.join("project/src/Token.sol"), "").unwrap();
574 fs::create_dir(dir_path.join("project/node_modules")).unwrap();
575
576 fs::create_dir(dir_path.join("dependency")).unwrap();
577 fs::write(dir_path.join("dependency/Math.sol"), "").unwrap();
578
579 std::os::unix::fs::symlink(
580 dir_path.join("dependency"),
581 dir_path.join("project/node_modules/dependency"),
582 )
583 .unwrap();
584
585 let cwd = dir_path.join("project/src");
587
588 assert_eq!(
589 normalize_solidity_import_path(&cwd, "../node_modules/dependency/Math.sol".as_ref())
590 .unwrap(),
591 dir_path.join("project/node_modules/dependency/Math.sol"),
592 );
593 }
594
595 #[test]
596 fn can_clean_solidity_path() {
597 let clean_solidity_path = |s: &str| clean_solidity_path(s.as_ref());
598 assert_eq!(clean_solidity_path("a"), PathBuf::from("a"));
599 assert_eq!(clean_solidity_path("./a"), PathBuf::from("a"));
600 assert_eq!(clean_solidity_path("../a"), PathBuf::from("../a"));
601 assert_eq!(clean_solidity_path("/a/"), PathBuf::from("/a"));
602 assert_eq!(clean_solidity_path("//a"), PathBuf::from("/a"));
603 assert_eq!(clean_solidity_path("a/b"), PathBuf::from("a/b"));
604 assert_eq!(clean_solidity_path("a//b"), PathBuf::from("a/b"));
605 assert_eq!(clean_solidity_path("/a/b"), PathBuf::from("/a/b"));
606 assert_eq!(clean_solidity_path("a/./b"), PathBuf::from("a/b"));
607 assert_eq!(clean_solidity_path("a/././b"), PathBuf::from("a/b"));
608 assert_eq!(clean_solidity_path("/a/../b"), PathBuf::from("/b"));
609 assert_eq!(clean_solidity_path("a/./../b/."), PathBuf::from("b"));
610 assert_eq!(clean_solidity_path("a/b/c"), PathBuf::from("a/b/c"));
611 assert_eq!(clean_solidity_path("a/b/../c"), PathBuf::from("a/c"));
612 assert_eq!(clean_solidity_path("a/b/../../c"), PathBuf::from("c"));
613 assert_eq!(clean_solidity_path("a/b/../../../c"), PathBuf::from("../c"));
614 assert_eq!(
615 clean_solidity_path("a/../b/../../c/./Token.sol"),
616 PathBuf::from("../c/Token.sol")
617 );
618 }
619
620 #[test]
621 fn can_find_ancestor() {
622 let a = Path::new("/foo/bar/bar/test.txt");
623 let b = Path::new("/foo/bar/foo/example/constract.sol");
624 let expected = Path::new("/foo/bar");
625 assert_eq!(common_ancestor(a, b).unwrap(), expected.to_path_buf())
626 }
627
628 #[test]
629 fn no_common_ancestor_path() {
630 let a = Path::new("/foo/bar");
631 let b = Path::new("./bar/foo");
632 assert!(common_ancestor(a, b).is_none());
633 }
634
635 #[test]
636 fn can_find_all_ancestor() {
637 let a = Path::new("/foo/bar/foo/example.txt");
638 let b = Path::new("/foo/bar/foo/test.txt");
639 let c = Path::new("/foo/bar/bar/foo/bar");
640 let expected = Path::new("/foo/bar");
641 let paths = vec![a, b, c];
642 assert_eq!(common_ancestor_all(paths).unwrap(), expected.to_path_buf())
643 }
644}