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 const OSAKA_SOLC: Version = Version::new(0, 8, 29);
72
73pub static SUPPORTS_BASE_PATH: Lazy<VersionReq> =
75 Lazy::new(|| VersionReq::parse(">=0.6.9").unwrap());
76
77pub static SUPPORTS_INCLUDE_PATH: Lazy<VersionReq> =
79 Lazy::new(|| VersionReq::parse(">=0.8.8").unwrap());
80
81pub fn range_by_offset(range: &Range<usize>, offset: isize) -> Range<usize> {
83 Range {
84 start: offset.saturating_add(range.start as isize) as usize,
85 end: offset.saturating_add(range.end as isize) as usize,
86 }
87}
88
89pub fn source_name<'a>(source: &'a Path, root: &Path) -> &'a Path {
93 strip_prefix(source, root)
94}
95
96pub fn strip_prefix<'a>(source: &'a Path, root: &Path) -> &'a Path {
98 source.strip_prefix(root).unwrap_or(source)
99}
100
101pub fn strip_prefix_owned(source: PathBuf, root: &Path) -> PathBuf {
103 source.strip_prefix(root).map(Path::to_path_buf).unwrap_or(source)
104}
105
106pub fn is_local_source_name(libs: &[impl AsRef<Path>], source: impl AsRef<Path>) -> bool {
108 resolve_library(libs, source.as_ref()).is_none()
109}
110
111pub fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf, SolcIoError> {
115 let path = path.as_ref();
116 let res = dunce::canonicalize(path);
117 #[cfg(windows)]
118 let res = res.map(|p| {
119 use path_slash::PathBufExt;
120 PathBuf::from(p.to_slash_lossy().as_ref())
121 });
122 res.map_err(|err| SolcIoError::new(err, path))
123}
124
125pub fn normalize_solidity_import_path(
135 directory: &Path,
136 import_path: &Path,
137) -> Result<PathBuf, SolcIoError> {
138 let original = directory.join(import_path);
139 let cleaned = clean_solidity_path(&original);
140
141 let normalized = dunce::simplified(&cleaned);
143 #[cfg(windows)]
144 let normalized = {
145 use path_slash::PathExt;
146 PathBuf::from(normalized.to_slash_lossy().as_ref())
147 };
148 #[cfg(not(windows))]
149 let normalized = PathBuf::from(normalized);
150
151 let _ = normalized.metadata().map_err(|err| SolcIoError::new(err, original))?;
153 Ok(normalized)
154}
155
156fn clean_solidity_path(original_path: &Path) -> PathBuf {
186 let mut new_path = Vec::new();
187
188 for component in original_path.components() {
189 match component {
190 Component::Prefix(..) | Component::RootDir | Component::Normal(..) => {
191 new_path.push(component);
192 }
193 Component::CurDir => {}
194 Component::ParentDir => {
195 if let Some(Component::Normal(..)) = new_path.last() {
196 new_path.pop();
197 } else {
198 new_path.push(component);
199 }
200 }
201 }
202 }
203
204 new_path.iter().collect()
205}
206
207pub fn canonicalized(path: impl Into<PathBuf>) -> PathBuf {
217 let path = path.into();
218 canonicalize(&path).unwrap_or(path)
219}
220
221pub fn resolve_library(libs: &[impl AsRef<Path>], source: impl AsRef<Path>) -> Option<PathBuf> {
225 let source = source.as_ref();
226 let comp = source.components().next()?;
227 match comp {
228 Component::Normal(first_dir) => {
229 for lib in libs {
232 let lib = lib.as_ref();
233 let contract = lib.join(source);
234 if contract.exists() {
235 return Some(contract);
237 }
238 let contract = lib
240 .join(first_dir)
241 .join("src")
242 .join(source.strip_prefix(first_dir).expect("is first component"));
243 if contract.exists() {
244 return Some(contract);
245 }
246 }
247 None
248 }
249 Component::RootDir => Some(source.into()),
250 _ => None,
251 }
252}
253
254pub fn resolve_absolute_library(
271 root: &Path,
272 cwd: &Path,
273 import: &Path,
274) -> Option<(PathBuf, PathBuf)> {
275 let mut parent = cwd.parent()?;
276 while parent != root {
277 if let Ok(import) = normalize_solidity_import_path(parent, import) {
278 return Some((parent.to_path_buf(), import));
279 }
280 parent = parent.parent()?;
281 }
282 None
283}
284
285pub fn library_fully_qualified_placeholder(name: &str) -> String {
290 name.chars().chain(std::iter::repeat('_')).take(36).collect()
291}
292
293pub fn library_hash_placeholder(name: impl AsRef<[u8]>) -> String {
295 let mut s = String::with_capacity(34 + 2);
296 s.push('$');
297 s.push_str(hex::Buffer::<17, false>::new().format(&library_hash(name)));
298 s.push('$');
299 s
300}
301
302pub fn library_hash(name: impl AsRef<[u8]>) -> [u8; 17] {
308 let hash = keccak256(name);
309 hash[..17].try_into().unwrap()
310}
311
312pub fn common_ancestor_all<I, P>(paths: I) -> Option<PathBuf>
327where
328 I: IntoIterator<Item = P>,
329 P: AsRef<Path>,
330{
331 let mut iter = paths.into_iter();
332 let mut ret = iter.next()?.as_ref().to_path_buf();
333 for path in iter {
334 if let Some(r) = common_ancestor(&ret, path.as_ref()) {
335 ret = r;
336 } else {
337 return None;
338 }
339 }
340 Some(ret)
341}
342
343pub fn common_ancestor(a: &Path, b: &Path) -> Option<PathBuf> {
357 let a = a.components();
358 let b = b.components();
359 let mut ret = PathBuf::new();
360 let mut found = false;
361 for (c1, c2) in a.zip(b) {
362 if c1 == c2 {
363 ret.push(c1);
364 found = true;
365 } else {
366 break;
367 }
368 }
369 if found {
370 Some(ret)
371 } else {
372 None
373 }
374}
375
376pub fn find_fave_or_alt_path(root: &Path, fave: &str, alt: &str) -> PathBuf {
381 let p = root.join(fave);
382 if !p.exists() {
383 let alt = root.join(alt);
384 if alt.exists() {
385 return alt;
386 }
387 }
388 p
389}
390
391cfg_if! {
392 if #[cfg(any(feature = "async", feature = "svm-solc"))] {
393 use tokio::runtime::{Handle, Runtime};
394
395 #[derive(Debug)]
396 pub enum RuntimeOrHandle {
397 Runtime(Runtime),
398 Handle(Handle),
399 }
400
401 impl Default for RuntimeOrHandle {
402 fn default() -> Self {
403 Self::new()
404 }
405 }
406
407 impl RuntimeOrHandle {
408 pub fn new() -> Self {
409 match Handle::try_current() {
410 Ok(handle) => Self::Handle(handle),
411 Err(_) => Self::Runtime(Runtime::new().expect("Failed to start runtime")),
412 }
413 }
414
415 pub fn block_on<F: std::future::Future>(&self, f: F) -> F::Output {
416 match &self {
417 Self::Runtime(runtime) => runtime.block_on(f),
418 Self::Handle(handle) => tokio::task::block_in_place(|| handle.block_on(f)),
419 }
420 }
421 }
422 }
423}
424
425#[cfg(any(test, feature = "project-util", feature = "test-utils"))]
427pub fn tempdir(name: &str) -> Result<tempfile::TempDir, SolcIoError> {
428 tempfile::Builder::new().prefix(name).tempdir().map_err(|err| SolcIoError::new(err, name))
429}
430
431pub fn read_json_file<T: DeserializeOwned>(path: &Path) -> Result<T, SolcError> {
433 let s = fs::read_to_string(path).map_err(|err| SolcError::io(err, path))?;
435 serde_json::from_str(&s).map_err(Into::into)
436}
437
438pub fn write_json_file<T: Serialize>(
440 value: &T,
441 path: &Path,
442 capacity: usize,
443) -> Result<(), SolcError> {
444 let file = fs::File::create(path).map_err(|err| SolcError::io(err, path))?;
445 let mut writer = std::io::BufWriter::with_capacity(capacity, file);
446 serde_json::to_writer(&mut writer, value)?;
447 writer.flush().map_err(|e| SolcError::io(e, path))
448}
449
450pub fn create_parent_dir_all(file: &Path) -> Result<(), SolcError> {
454 if let Some(parent) = file.parent() {
455 fs::create_dir_all(parent).map_err(|err| {
456 SolcError::msg(format!(
457 "Failed to create artifact parent folder \"{}\": {}",
458 parent.display(),
459 err
460 ))
461 })?;
462 }
463 Ok(())
464}
465
466#[cfg(any(test, feature = "test-utils"))]
467pub fn touch(path: &std::path::Path) -> std::io::Result<()> {
469 match std::fs::OpenOptions::new().create(true).write(true).truncate(false).open(path) {
470 Ok(_) => Ok(()),
471 Err(e) => Err(e),
472 }
473}
474
475#[cfg(any(test, feature = "test-utils"))]
476pub fn mkdir_or_touch(tmp: &std::path::Path, paths: &[&str]) {
477 for path in paths {
478 if let Some(parent) = Path::new(path).parent() {
479 std::fs::create_dir_all(tmp.join(parent)).unwrap();
480 }
481 if path.ends_with(".sol") {
482 let path = tmp.join(path);
483 touch(&path).unwrap();
484 } else {
485 let path: PathBuf = tmp.join(path);
486 std::fs::create_dir_all(path).unwrap();
487 }
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 pub use super::*;
494 pub use std::fs::{create_dir_all, File};
495
496 #[test]
497 fn can_create_parent_dirs_with_ext() {
498 let tmp_dir = tempdir("out").unwrap();
499 let path = tmp_dir.path().join("IsolationModeMagic.sol/IsolationModeMagic.json");
500 create_parent_dir_all(&path).unwrap();
501 assert!(path.parent().unwrap().is_dir());
502 }
503
504 #[test]
505 fn can_create_parent_dirs_versioned() {
506 let tmp_dir = tempdir("out").unwrap();
507 let path = tmp_dir.path().join("IVersioned.sol/IVersioned.0.8.16.json");
508 create_parent_dir_all(&path).unwrap();
509 assert!(path.parent().unwrap().is_dir());
510 let path = tmp_dir.path().join("IVersioned.sol/IVersioned.json");
511 create_parent_dir_all(&path).unwrap();
512 assert!(path.parent().unwrap().is_dir());
513 }
514
515 #[test]
516 fn can_determine_local_paths() {
517 assert!(is_local_source_name(&[""], "./local/contract.sol"));
518 assert!(is_local_source_name(&[""], "../local/contract.sol"));
519 assert!(!is_local_source_name(&[""], "/ds-test/test.sol"));
520
521 let tmp_dir = tempdir("contracts").unwrap();
522 let dir = tmp_dir.path().join("ds-test");
523 create_dir_all(&dir).unwrap();
524 File::create(dir.join("test.sol")).unwrap();
525
526 assert!(!is_local_source_name(&[tmp_dir.path()], "ds-test/test.sol"));
527 }
528
529 #[test]
530 fn can_normalize_solidity_import_path() {
531 let dir = tempfile::tempdir().unwrap();
532 let dir_path = dir.path();
533
534 fs::create_dir_all(dir_path.join("src/common")).unwrap();
543 fs::write(dir_path.join("src/Token.sol"), "").unwrap();
544 fs::write(dir_path.join("src/common/Burnable.sol"), "").unwrap();
545
546 let cwd = dir_path.join("src");
548
549 assert_eq!(
550 normalize_solidity_import_path(&cwd, "./common/Burnable.sol".as_ref()).unwrap(),
551 dir_path.join("src/common/Burnable.sol"),
552 );
553
554 assert!(normalize_solidity_import_path(&cwd, "./common/Pausable.sol".as_ref()).is_err());
555 }
556
557 #[test]
560 #[cfg(unix)]
561 fn can_normalize_solidity_import_path_symlink() {
562 let dir = tempfile::tempdir().unwrap();
563 let dir_path = dir.path();
564
565 fs::create_dir_all(dir_path.join("project/src")).unwrap();
577 fs::write(dir_path.join("project/src/Token.sol"), "").unwrap();
578 fs::create_dir(dir_path.join("project/node_modules")).unwrap();
579
580 fs::create_dir(dir_path.join("dependency")).unwrap();
581 fs::write(dir_path.join("dependency/Math.sol"), "").unwrap();
582
583 std::os::unix::fs::symlink(
584 dir_path.join("dependency"),
585 dir_path.join("project/node_modules/dependency"),
586 )
587 .unwrap();
588
589 let cwd = dir_path.join("project/src");
591
592 assert_eq!(
593 normalize_solidity_import_path(&cwd, "../node_modules/dependency/Math.sol".as_ref())
594 .unwrap(),
595 dir_path.join("project/node_modules/dependency/Math.sol"),
596 );
597 }
598
599 #[test]
600 fn can_clean_solidity_path() {
601 let clean_solidity_path = |s: &str| clean_solidity_path(s.as_ref());
602 assert_eq!(clean_solidity_path("a"), PathBuf::from("a"));
603 assert_eq!(clean_solidity_path("./a"), PathBuf::from("a"));
604 assert_eq!(clean_solidity_path("../a"), PathBuf::from("../a"));
605 assert_eq!(clean_solidity_path("/a/"), PathBuf::from("/a"));
606 assert_eq!(clean_solidity_path("//a"), PathBuf::from("/a"));
607 assert_eq!(clean_solidity_path("a/b"), PathBuf::from("a/b"));
608 assert_eq!(clean_solidity_path("a//b"), PathBuf::from("a/b"));
609 assert_eq!(clean_solidity_path("/a/b"), PathBuf::from("/a/b"));
610 assert_eq!(clean_solidity_path("a/./b"), PathBuf::from("a/b"));
611 assert_eq!(clean_solidity_path("a/././b"), PathBuf::from("a/b"));
612 assert_eq!(clean_solidity_path("/a/../b"), PathBuf::from("/b"));
613 assert_eq!(clean_solidity_path("a/./../b/."), PathBuf::from("b"));
614 assert_eq!(clean_solidity_path("a/b/c"), PathBuf::from("a/b/c"));
615 assert_eq!(clean_solidity_path("a/b/../c"), PathBuf::from("a/c"));
616 assert_eq!(clean_solidity_path("a/b/../../c"), PathBuf::from("c"));
617 assert_eq!(clean_solidity_path("a/b/../../../c"), PathBuf::from("../c"));
618 assert_eq!(
619 clean_solidity_path("a/../b/../../c/./Token.sol"),
620 PathBuf::from("../c/Token.sol")
621 );
622 }
623
624 #[test]
625 fn can_find_ancestor() {
626 let a = Path::new("/foo/bar/bar/test.txt");
627 let b = Path::new("/foo/bar/foo/example/constract.sol");
628 let expected = Path::new("/foo/bar");
629 assert_eq!(common_ancestor(a, b).unwrap(), expected.to_path_buf())
630 }
631
632 #[test]
633 fn no_common_ancestor_path() {
634 let a = Path::new("/foo/bar");
635 let b = Path::new("./bar/foo");
636 assert!(common_ancestor(a, b).is_none());
637 }
638
639 #[test]
640 fn can_find_all_ancestor() {
641 let a = Path::new("/foo/bar/foo/example.txt");
642 let b = Path::new("/foo/bar/foo/test.txt");
643 let c = Path::new("/foo/bar/bar/foo/bar");
644 let expected = Path::new("/foo/bar");
645 let paths = vec![a, b, c];
646 assert_eq!(common_ancestor_all(paths).unwrap(), expected.to_path_buf())
647 }
648}