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
81#[cfg(feature = "hasher")]
86pub fn unique_hash(input: impl AsRef<[u8]>) -> String {
87 encode_hash(xxhash_rust::xxh3::xxh3_64(input.as_ref()))
88}
89
90#[cfg(feature = "hasher")]
94pub fn unique_hash_many(inputs: impl IntoIterator<Item = impl AsRef<[u8]>>) -> String {
95 let mut hasher = xxhash_rust::xxh3::Xxh3Default::new();
96 for input in inputs {
97 hasher.update(input.as_ref());
98 }
99 encode_hash(hasher.digest())
100}
101
102#[cfg(feature = "hasher")]
103fn encode_hash(x: u64) -> String {
104 hex::encode(x.to_be_bytes())
105}
106
107pub fn range_by_offset(range: &Range<usize>, offset: isize) -> Range<usize> {
109 Range {
110 start: offset.saturating_add(range.start as isize) as usize,
111 end: offset.saturating_add(range.end as isize) as usize,
112 }
113}
114
115pub fn source_name<'a>(source: &'a Path, root: &Path) -> &'a Path {
119 strip_prefix(source, root)
120}
121
122pub fn strip_prefix<'a>(source: &'a Path, root: &Path) -> &'a Path {
124 source.strip_prefix(root).unwrap_or(source)
125}
126
127pub fn strip_prefix_owned(source: PathBuf, root: &Path) -> PathBuf {
129 source.strip_prefix(root).map(Path::to_path_buf).unwrap_or(source)
130}
131
132pub fn is_local_source_name(libs: &[impl AsRef<Path>], source: impl AsRef<Path>) -> bool {
134 resolve_library(libs, source.as_ref()).is_none()
135}
136
137pub fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf, SolcIoError> {
141 let path = path.as_ref();
142 let res = dunce::canonicalize(path);
143 #[cfg(windows)]
144 let res = res.map(|p| {
145 use path_slash::PathBufExt;
146 PathBuf::from(p.to_slash_lossy().as_ref())
147 });
148 res.map_err(|err| SolcIoError::new(err, path))
149}
150
151pub fn normalize_solidity_import_path(
161 directory: &Path,
162 import_path: &Path,
163) -> Result<PathBuf, SolcIoError> {
164 let original = directory.join(import_path);
165 let cleaned = clean_solidity_path(&original);
166
167 let normalized = dunce::simplified(&cleaned);
169 #[cfg(windows)]
170 let normalized = {
171 use path_slash::PathExt;
172 PathBuf::from(normalized.to_slash_lossy().as_ref())
173 };
174 #[cfg(not(windows))]
175 let normalized = PathBuf::from(normalized);
176
177 let _ = normalized.metadata().map_err(|err| SolcIoError::new(err, original))?;
179 Ok(normalized)
180}
181
182fn clean_solidity_path(original_path: &Path) -> PathBuf {
212 let mut new_path = Vec::new();
213
214 for component in original_path.components() {
215 match component {
216 Component::Prefix(..) | Component::RootDir | Component::Normal(..) => {
217 new_path.push(component);
218 }
219 Component::CurDir => {}
220 Component::ParentDir => {
221 if let Some(Component::Normal(..)) = new_path.last() {
222 new_path.pop();
223 } else {
224 new_path.push(component);
225 }
226 }
227 }
228 }
229
230 new_path.iter().collect()
231}
232
233pub fn canonicalized(path: impl Into<PathBuf>) -> PathBuf {
243 let path = path.into();
244 canonicalize(&path).unwrap_or(path)
245}
246
247pub fn resolve_library(libs: &[impl AsRef<Path>], source: impl AsRef<Path>) -> Option<PathBuf> {
251 let source = source.as_ref();
252 let comp = source.components().next()?;
253 match comp {
254 Component::Normal(first_dir) => {
255 for lib in libs {
258 let lib = lib.as_ref();
259 let contract = lib.join(source);
260 if contract.exists() {
261 return Some(contract);
263 }
264 let contract = lib
266 .join(first_dir)
267 .join("src")
268 .join(source.strip_prefix(first_dir).expect("is first component"));
269 if contract.exists() {
270 return Some(contract);
271 }
272 }
273 None
274 }
275 Component::RootDir => Some(source.into()),
276 _ => None,
277 }
278}
279
280pub fn resolve_absolute_library(
297 root: &Path,
298 cwd: &Path,
299 import: &Path,
300) -> Option<(PathBuf, PathBuf)> {
301 let mut parent = cwd.parent()?;
302 while parent != root {
303 if let Ok(import) = normalize_solidity_import_path(parent, import) {
304 return Some((parent.to_path_buf(), import));
305 }
306 parent = parent.parent()?;
307 }
308 None
309}
310
311pub fn library_fully_qualified_placeholder(name: &str) -> String {
316 name.chars().chain(std::iter::repeat('_')).take(36).collect()
317}
318
319pub fn library_hash_placeholder(name: impl AsRef<[u8]>) -> String {
321 let mut s = String::with_capacity(34 + 2);
322 s.push('$');
323 s.push_str(hex::Buffer::<17, false>::new().format(&library_hash(name)));
324 s.push('$');
325 s
326}
327
328pub fn library_hash(name: impl AsRef<[u8]>) -> [u8; 17] {
334 let hash = keccak256(name);
335 hash[..17].try_into().unwrap()
336}
337
338pub fn common_ancestor_all<I, P>(paths: I) -> Option<PathBuf>
353where
354 I: IntoIterator<Item = P>,
355 P: AsRef<Path>,
356{
357 let mut iter = paths.into_iter();
358 let mut ret = iter.next()?.as_ref().to_path_buf();
359 for path in iter {
360 if let Some(r) = common_ancestor(&ret, path.as_ref()) {
361 ret = r;
362 } else {
363 return None;
364 }
365 }
366 Some(ret)
367}
368
369pub fn common_ancestor(a: &Path, b: &Path) -> Option<PathBuf> {
383 let a = a.components();
384 let b = b.components();
385 let mut ret = PathBuf::new();
386 let mut found = false;
387 for (c1, c2) in a.zip(b) {
388 if c1 == c2 {
389 ret.push(c1);
390 found = true;
391 } else {
392 break;
393 }
394 }
395 if found {
396 Some(ret)
397 } else {
398 None
399 }
400}
401
402pub fn find_fave_or_alt_path(root: &Path, fave: &str, alt: &str) -> PathBuf {
407 let p = root.join(fave);
408 if !p.exists() {
409 let alt = root.join(alt);
410 if alt.exists() {
411 return alt;
412 }
413 }
414 p
415}
416
417cfg_if! {
418 if #[cfg(any(feature = "async", feature = "svm-solc"))] {
419 use tokio::runtime::{Handle, Runtime};
420
421 #[derive(Debug)]
422 pub enum RuntimeOrHandle {
423 Runtime(Runtime),
424 Handle(Handle),
425 }
426
427 impl Default for RuntimeOrHandle {
428 fn default() -> Self {
429 Self::new()
430 }
431 }
432
433 impl RuntimeOrHandle {
434 pub fn new() -> Self {
435 match Handle::try_current() {
436 Ok(handle) => Self::Handle(handle),
437 Err(_) => Self::Runtime(Runtime::new().expect("Failed to start runtime")),
438 }
439 }
440
441 pub fn block_on<F: std::future::Future>(&self, f: F) -> F::Output {
442 match &self {
443 Self::Runtime(runtime) => runtime.block_on(f),
444 Self::Handle(handle) => tokio::task::block_in_place(|| handle.block_on(f)),
445 }
446 }
447 }
448 }
449}
450
451#[cfg(any(test, feature = "project-util", feature = "test-utils"))]
453pub fn tempdir(name: &str) -> Result<tempfile::TempDir, SolcIoError> {
454 tempfile::Builder::new().prefix(name).tempdir().map_err(|err| SolcIoError::new(err, name))
455}
456
457pub fn read_json_file<T: DeserializeOwned>(path: &Path) -> Result<T, SolcError> {
459 let s = fs::read_to_string(path).map_err(|err| SolcError::io(err, path))?;
461 serde_json::from_str(&s).map_err(Into::into)
462}
463
464pub fn write_json_file<T: Serialize>(
466 value: &T,
467 path: &Path,
468 capacity: usize,
469) -> Result<(), SolcError> {
470 let file = fs::File::create(path).map_err(|err| SolcError::io(err, path))?;
471 let mut writer = std::io::BufWriter::with_capacity(capacity, file);
472 serde_json::to_writer(&mut writer, value)?;
473 writer.flush().map_err(|e| SolcError::io(e, path))
474}
475
476pub fn create_parent_dir_all(file: &Path) -> Result<(), SolcError> {
480 if let Some(parent) = file.parent() {
481 fs::create_dir_all(parent).map_err(|err| {
482 SolcError::msg(format!(
483 "Failed to create artifact parent folder \"{}\": {}",
484 parent.display(),
485 err
486 ))
487 })?;
488 }
489 Ok(())
490}
491
492#[cfg(any(test, feature = "test-utils"))]
493pub fn touch(path: &std::path::Path) -> std::io::Result<()> {
495 match std::fs::OpenOptions::new().create(true).write(true).truncate(false).open(path) {
496 Ok(_) => Ok(()),
497 Err(e) => Err(e),
498 }
499}
500
501#[cfg(any(test, feature = "test-utils"))]
502pub fn mkdir_or_touch(tmp: &std::path::Path, paths: &[&str]) {
503 for path in paths {
504 if let Some(parent) = Path::new(path).parent() {
505 std::fs::create_dir_all(tmp.join(parent)).unwrap();
506 }
507 if path.ends_with(".sol") {
508 let path = tmp.join(path);
509 touch(&path).unwrap();
510 } else {
511 let path: PathBuf = tmp.join(path);
512 std::fs::create_dir_all(path).unwrap();
513 }
514 }
515}
516
517#[cfg(test)]
518mod tests {
519 pub use super::*;
520 pub use std::fs::{create_dir_all, File};
521
522 #[test]
523 fn can_create_parent_dirs_with_ext() {
524 let tmp_dir = tempdir("out").unwrap();
525 let path = tmp_dir.path().join("IsolationModeMagic.sol/IsolationModeMagic.json");
526 create_parent_dir_all(&path).unwrap();
527 assert!(path.parent().unwrap().is_dir());
528 }
529
530 #[test]
531 fn can_create_parent_dirs_versioned() {
532 let tmp_dir = tempdir("out").unwrap();
533 let path = tmp_dir.path().join("IVersioned.sol/IVersioned.0.8.16.json");
534 create_parent_dir_all(&path).unwrap();
535 assert!(path.parent().unwrap().is_dir());
536 let path = tmp_dir.path().join("IVersioned.sol/IVersioned.json");
537 create_parent_dir_all(&path).unwrap();
538 assert!(path.parent().unwrap().is_dir());
539 }
540
541 #[test]
542 fn can_determine_local_paths() {
543 assert!(is_local_source_name(&[""], "./local/contract.sol"));
544 assert!(is_local_source_name(&[""], "../local/contract.sol"));
545 assert!(!is_local_source_name(&[""], "/ds-test/test.sol"));
546
547 let tmp_dir = tempdir("contracts").unwrap();
548 let dir = tmp_dir.path().join("ds-test");
549 create_dir_all(&dir).unwrap();
550 File::create(dir.join("test.sol")).unwrap();
551
552 assert!(!is_local_source_name(&[tmp_dir.path()], "ds-test/test.sol"));
553 }
554
555 #[test]
556 fn can_normalize_solidity_import_path() {
557 let dir = tempfile::tempdir().unwrap();
558 let dir_path = dir.path();
559
560 fs::create_dir_all(dir_path.join("src/common")).unwrap();
569 fs::write(dir_path.join("src/Token.sol"), "").unwrap();
570 fs::write(dir_path.join("src/common/Burnable.sol"), "").unwrap();
571
572 let cwd = dir_path.join("src");
574
575 assert_eq!(
576 normalize_solidity_import_path(&cwd, "./common/Burnable.sol".as_ref()).unwrap(),
577 dir_path.join("src/common/Burnable.sol"),
578 );
579
580 assert!(normalize_solidity_import_path(&cwd, "./common/Pausable.sol".as_ref()).is_err());
581 }
582
583 #[test]
586 #[cfg(unix)]
587 fn can_normalize_solidity_import_path_symlink() {
588 let dir = tempfile::tempdir().unwrap();
589 let dir_path = dir.path();
590
591 fs::create_dir_all(dir_path.join("project/src")).unwrap();
603 fs::write(dir_path.join("project/src/Token.sol"), "").unwrap();
604 fs::create_dir(dir_path.join("project/node_modules")).unwrap();
605
606 fs::create_dir(dir_path.join("dependency")).unwrap();
607 fs::write(dir_path.join("dependency/Math.sol"), "").unwrap();
608
609 std::os::unix::fs::symlink(
610 dir_path.join("dependency"),
611 dir_path.join("project/node_modules/dependency"),
612 )
613 .unwrap();
614
615 let cwd = dir_path.join("project/src");
617
618 assert_eq!(
619 normalize_solidity_import_path(&cwd, "../node_modules/dependency/Math.sol".as_ref())
620 .unwrap(),
621 dir_path.join("project/node_modules/dependency/Math.sol"),
622 );
623 }
624
625 #[test]
626 fn can_clean_solidity_path() {
627 let clean_solidity_path = |s: &str| clean_solidity_path(s.as_ref());
628 assert_eq!(clean_solidity_path("a"), PathBuf::from("a"));
629 assert_eq!(clean_solidity_path("./a"), PathBuf::from("a"));
630 assert_eq!(clean_solidity_path("../a"), PathBuf::from("../a"));
631 assert_eq!(clean_solidity_path("/a/"), PathBuf::from("/a"));
632 assert_eq!(clean_solidity_path("//a"), PathBuf::from("/a"));
633 assert_eq!(clean_solidity_path("a/b"), PathBuf::from("a/b"));
634 assert_eq!(clean_solidity_path("a//b"), PathBuf::from("a/b"));
635 assert_eq!(clean_solidity_path("/a/b"), PathBuf::from("/a/b"));
636 assert_eq!(clean_solidity_path("a/./b"), PathBuf::from("a/b"));
637 assert_eq!(clean_solidity_path("a/././b"), PathBuf::from("a/b"));
638 assert_eq!(clean_solidity_path("/a/../b"), PathBuf::from("/b"));
639 assert_eq!(clean_solidity_path("a/./../b/."), PathBuf::from("b"));
640 assert_eq!(clean_solidity_path("a/b/c"), PathBuf::from("a/b/c"));
641 assert_eq!(clean_solidity_path("a/b/../c"), PathBuf::from("a/c"));
642 assert_eq!(clean_solidity_path("a/b/../../c"), PathBuf::from("c"));
643 assert_eq!(clean_solidity_path("a/b/../../../c"), PathBuf::from("../c"));
644 assert_eq!(
645 clean_solidity_path("a/../b/../../c/./Token.sol"),
646 PathBuf::from("../c/Token.sol")
647 );
648 }
649
650 #[test]
651 fn can_find_ancestor() {
652 let a = Path::new("/foo/bar/bar/test.txt");
653 let b = Path::new("/foo/bar/foo/example/constract.sol");
654 let expected = Path::new("/foo/bar");
655 assert_eq!(common_ancestor(a, b).unwrap(), expected.to_path_buf())
656 }
657
658 #[test]
659 fn no_common_ancestor_path() {
660 let a = Path::new("/foo/bar");
661 let b = Path::new("./bar/foo");
662 assert!(common_ancestor(a, b).is_none());
663 }
664
665 #[test]
666 fn can_find_all_ancestor() {
667 let a = Path::new("/foo/bar/foo/example.txt");
668 let b = Path::new("/foo/bar/foo/test.txt");
669 let c = Path::new("/foo/bar/bar/foo/bar");
670 let expected = Path::new("/foo/bar");
671 let paths = vec![a, b, c];
672 assert_eq!(common_ancestor_all(paths).unwrap(), expected.to_path_buf())
673 }
674}