foundry_compilers_core/utils/
mod.rs1use crate::error::{SolcError, SolcIoError};
4use alloy_primitives::{hex, keccak256};
5use semver::{Version, VersionReq};
6use serde::{Serialize, de::DeserializeOwned};
7use std::{
8 fs,
9 io::Write,
10 ops::Range,
11 path::{Component, Path, PathBuf},
12 sync::LazyLock as Lazy,
13};
14
15#[cfg(feature = "regex")]
16mod re;
17#[cfg(feature = "regex")]
18pub use re::*;
19
20#[cfg(feature = "walkdir")]
21mod wd;
22#[cfg(feature = "walkdir")]
23pub use wd::*;
24
25pub const SOLC_EXTENSIONS: &[&str] = &["sol", "yul"];
27
28pub const BYZANTIUM_SOLC: Version = Version::new(0, 4, 21);
31
32pub const CONSTANTINOPLE_SOLC: Version = Version::new(0, 4, 22);
35
36pub const PETERSBURG_SOLC: Version = Version::new(0, 5, 5);
39
40pub const ISTANBUL_SOLC: Version = Version::new(0, 5, 14);
43
44pub const BERLIN_SOLC: Version = Version::new(0, 8, 5);
47
48pub const LONDON_SOLC: Version = Version::new(0, 8, 7);
51
52pub const PARIS_SOLC: Version = Version::new(0, 8, 18);
55
56pub const SHANGHAI_SOLC: Version = Version::new(0, 8, 20);
59
60pub const CANCUN_SOLC: Version = Version::new(0, 8, 24);
63
64pub const PRAGUE_SOLC: Version = Version::new(0, 8, 27);
67
68pub const OSAKA_SOLC: Version = Version::new(0, 8, 29);
71
72pub static SUPPORTS_BASE_PATH: Lazy<VersionReq> =
74 Lazy::new(|| VersionReq::parse(">=0.6.9").unwrap());
75
76pub static SUPPORTS_INCLUDE_PATH: Lazy<VersionReq> =
78 Lazy::new(|| VersionReq::parse(">=0.8.8").unwrap());
79
80#[cfg(feature = "hasher")]
85pub fn unique_hash(input: impl AsRef<[u8]>) -> String {
86 encode_hash(xxhash_rust::xxh3::xxh3_64(input.as_ref()))
87}
88
89#[cfg(feature = "hasher")]
93pub fn unique_hash_many(inputs: impl IntoIterator<Item = impl AsRef<[u8]>>) -> String {
94 let mut hasher = xxhash_rust::xxh3::Xxh3Default::new();
95 for input in inputs {
96 hasher.update(input.as_ref());
97 }
98 encode_hash(hasher.digest())
99}
100
101#[cfg(feature = "hasher")]
102fn encode_hash(x: u64) -> String {
103 hex::encode(x.to_be_bytes())
104}
105
106pub const fn range_by_offset(range: &Range<usize>, offset: isize) -> Range<usize> {
108 Range {
109 start: offset.saturating_add(range.start as isize) as usize,
110 end: offset.saturating_add(range.end as isize) as usize,
111 }
112}
113
114pub fn source_name<'a>(source: &'a Path, root: &Path) -> &'a Path {
118 strip_prefix(source, root)
119}
120
121pub fn strip_prefix<'a>(source: &'a Path, root: &Path) -> &'a Path {
123 source.strip_prefix(root).unwrap_or(source)
124}
125
126pub fn strip_prefix_owned(source: PathBuf, root: &Path) -> PathBuf {
128 source.strip_prefix(root).map(Path::to_path_buf).unwrap_or(source)
129}
130
131pub fn is_local_source_name(libs: &[impl AsRef<Path>], source: impl AsRef<Path>) -> bool {
133 resolve_library(libs, source.as_ref()).is_none()
134}
135
136pub fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf, SolcIoError> {
140 let path = path.as_ref();
141 let res = dunce::canonicalize(path);
142 #[cfg(windows)]
143 let res = res.map(|p| {
144 use path_slash::PathBufExt;
145 PathBuf::from(p.to_slash_lossy().as_ref())
146 });
147 res.map_err(|err| SolcIoError::new(err, path))
148}
149
150pub fn normalize_solidity_import_path(
160 directory: &Path,
161 import_path: &Path,
162) -> Result<PathBuf, SolcIoError> {
163 let original = directory.join(import_path);
164 let cleaned = clean_solidity_path(&original);
165
166 let normalized = dunce::simplified(&cleaned);
168 #[cfg(windows)]
169 let normalized = {
170 use path_slash::PathExt;
171 PathBuf::from(normalized.to_slash_lossy().as_ref())
172 };
173 #[cfg(not(windows))]
174 let normalized = PathBuf::from(normalized);
175
176 let _ = normalized.metadata().map_err(|err| SolcIoError::new(err, original))?;
178 Ok(normalized)
179}
180
181fn clean_solidity_path(original_path: &Path) -> PathBuf {
211 let mut new_path = Vec::new();
212
213 for component in original_path.components() {
214 match component {
215 Component::Prefix(..) | Component::RootDir | Component::Normal(..) => {
216 new_path.push(component);
217 }
218 Component::CurDir => {}
219 Component::ParentDir => {
220 if let Some(Component::Normal(..)) = new_path.last() {
221 new_path.pop();
222 } else {
223 new_path.push(component);
224 }
225 }
226 }
227 }
228
229 new_path.iter().collect()
230}
231
232pub fn canonicalized(path: impl Into<PathBuf>) -> PathBuf {
242 let path = path.into();
243 canonicalize(&path).unwrap_or(path)
244}
245
246pub fn resolve_library(libs: &[impl AsRef<Path>], source: impl AsRef<Path>) -> Option<PathBuf> {
250 let source = source.as_ref();
251 let comp = source.components().next()?;
252 match comp {
253 Component::Normal(first_dir) => {
254 for lib in libs {
257 let lib = lib.as_ref();
258 let contract = lib.join(source);
259 if contract.exists() {
260 return Some(contract);
262 }
263 let contract = lib
265 .join(first_dir)
266 .join("src")
267 .join(source.strip_prefix(first_dir).expect("is first component"));
268 if contract.exists() {
269 return Some(contract);
270 }
271 }
272 None
273 }
274 Component::RootDir => Some(source.into()),
275 _ => None,
276 }
277}
278
279pub fn resolve_absolute_library(
296 root: &Path,
297 cwd: &Path,
298 import: &Path,
299) -> Option<(PathBuf, PathBuf)> {
300 let mut parent = cwd.parent()?;
301 while parent != root {
302 if let Ok(import) = normalize_solidity_import_path(parent, import) {
303 return Some((parent.to_path_buf(), import));
304 }
305 parent = parent.parent()?;
306 }
307 None
308}
309
310pub fn library_fully_qualified_placeholder(name: &str) -> String {
315 name.chars().chain(std::iter::repeat('_')).take(36).collect()
316}
317
318pub fn library_hash_placeholder(name: impl AsRef<[u8]>) -> String {
320 let mut s = String::with_capacity(34 + 2);
321 s.push('$');
322 s.push_str(hex::Buffer::<17, false>::new().format(&library_hash(name)));
323 s.push('$');
324 s
325}
326
327pub fn library_hash(name: impl AsRef<[u8]>) -> [u8; 17] {
333 let hash = keccak256(name);
334 hash[..17].try_into().unwrap()
335}
336
337pub fn common_ancestor_all<I, P>(paths: I) -> Option<PathBuf>
352where
353 I: IntoIterator<Item = P>,
354 P: AsRef<Path>,
355{
356 let mut iter = paths.into_iter();
357 let mut ret = iter.next()?.as_ref().to_path_buf();
358 for path in iter {
359 ret = common_ancestor(&ret, path.as_ref())?;
360 }
361 Some(ret)
362}
363
364pub fn common_ancestor(a: &Path, b: &Path) -> Option<PathBuf> {
378 let a = a.components();
379 let b = b.components();
380 let mut ret = PathBuf::new();
381 let mut found = false;
382 for (c1, c2) in a.zip(b) {
383 if c1 == c2 {
384 ret.push(c1);
385 found = true;
386 } else {
387 break;
388 }
389 }
390 found.then_some(ret)
391}
392
393pub fn find_fave_or_alt_path(root: &Path, fave: &str, alt: &str) -> PathBuf {
398 let p = root.join(fave);
399 if !p.exists() {
400 let alt = root.join(alt);
401 if alt.exists() {
402 return alt;
403 }
404 }
405 p
406}
407
408#[cfg(any(feature = "async", feature = "svm-solc"))]
409use tokio::runtime::{Handle, Runtime};
410
411#[cfg(any(feature = "async", feature = "svm-solc"))]
412#[derive(Debug)]
413pub enum RuntimeOrHandle {
414 Runtime(Runtime),
415 Handle(Handle),
416}
417
418#[cfg(any(feature = "async", feature = "svm-solc"))]
419impl Default for RuntimeOrHandle {
420 fn default() -> Self {
421 Self::new()
422 }
423}
424
425#[cfg(any(feature = "async", feature = "svm-solc"))]
426impl RuntimeOrHandle {
427 pub fn new() -> Self {
428 match Handle::try_current() {
429 Ok(handle) => Self::Handle(handle),
430 Err(_) => Self::Runtime(Runtime::new().expect("Failed to start runtime")),
431 }
432 }
433
434 pub fn block_on<F: std::future::Future>(&self, f: F) -> F::Output {
435 match &self {
436 Self::Runtime(runtime) => runtime.block_on(f),
437 Self::Handle(handle) => tokio::task::block_in_place(|| handle.block_on(f)),
438 }
439 }
440}
441
442#[cfg(any(test, feature = "project-util", feature = "test-utils"))]
444pub fn tempdir(name: &str) -> Result<tempfile::TempDir, SolcIoError> {
445 tempfile::Builder::new().prefix(name).tempdir().map_err(|err| SolcIoError::new(err, name))
446}
447
448pub fn read_json_file<T: DeserializeOwned>(path: &Path) -> Result<T, SolcError> {
450 let s = fs::read_to_string(path).map_err(|err| SolcError::io(err, path))?;
452 serde_json::from_str(&s).map_err(Into::into)
453}
454
455pub fn write_json_file<T: Serialize>(
457 value: &T,
458 path: &Path,
459 capacity: usize,
460) -> Result<(), SolcError> {
461 let file = fs::File::create(path).map_err(|err| SolcError::io(err, path))?;
462 let mut writer = std::io::BufWriter::with_capacity(capacity, file);
463 serde_json::to_writer(&mut writer, value)?;
464 writer.flush().map_err(|e| SolcError::io(e, path))
465}
466
467pub fn create_parent_dir_all(file: &Path) -> Result<(), SolcError> {
471 if let Some(parent) = file.parent() {
472 fs::create_dir_all(parent).map_err(|err| {
473 SolcError::msg(format!(
474 "Failed to create artifact parent folder \"{}\": {}",
475 parent.display(),
476 err
477 ))
478 })?;
479 }
480 Ok(())
481}
482
483#[cfg(any(test, feature = "test-utils"))]
484pub fn touch(path: &std::path::Path) -> std::io::Result<()> {
486 match std::fs::OpenOptions::new().create(true).write(true).truncate(false).open(path) {
487 Ok(_) => Ok(()),
488 Err(e) => Err(e),
489 }
490}
491
492#[cfg(any(test, feature = "test-utils"))]
493pub fn mkdir_or_touch(tmp: &std::path::Path, paths: &[&str]) {
494 for path in paths {
495 if let Some(parent) = Path::new(path).parent() {
496 std::fs::create_dir_all(tmp.join(parent)).unwrap();
497 }
498 if path.ends_with(".sol") {
499 let path = tmp.join(path);
500 touch(&path).unwrap();
501 } else {
502 let path: PathBuf = tmp.join(path);
503 std::fs::create_dir_all(path).unwrap();
504 }
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 pub use super::*;
511 pub use std::fs::{File, create_dir_all};
512
513 #[test]
514 fn can_create_parent_dirs_with_ext() {
515 let tmp_dir = tempdir("out").unwrap();
516 let path = tmp_dir.path().join("IsolationModeMagic.sol/IsolationModeMagic.json");
517 create_parent_dir_all(&path).unwrap();
518 assert!(path.parent().unwrap().is_dir());
519 }
520
521 #[test]
522 fn can_create_parent_dirs_versioned() {
523 let tmp_dir = tempdir("out").unwrap();
524 let path = tmp_dir.path().join("IVersioned.sol/IVersioned.0.8.16.json");
525 create_parent_dir_all(&path).unwrap();
526 assert!(path.parent().unwrap().is_dir());
527 let path = tmp_dir.path().join("IVersioned.sol/IVersioned.json");
528 create_parent_dir_all(&path).unwrap();
529 assert!(path.parent().unwrap().is_dir());
530 }
531
532 #[test]
533 fn can_determine_local_paths() {
534 assert!(is_local_source_name(&[""], "./local/contract.sol"));
535 assert!(is_local_source_name(&[""], "../local/contract.sol"));
536 assert!(!is_local_source_name(&[""], "/ds-test/test.sol"));
537
538 let tmp_dir = tempdir("contracts").unwrap();
539 let dir = tmp_dir.path().join("ds-test");
540 create_dir_all(&dir).unwrap();
541 File::create(dir.join("test.sol")).unwrap();
542
543 assert!(!is_local_source_name(&[tmp_dir.path()], "ds-test/test.sol"));
544 }
545
546 #[test]
547 fn can_normalize_solidity_import_path() {
548 let dir = tempfile::tempdir().unwrap();
549 let dir_path = dir.path();
550
551 fs::create_dir_all(dir_path.join("src/common")).unwrap();
560 fs::write(dir_path.join("src/Token.sol"), "").unwrap();
561 fs::write(dir_path.join("src/common/Burnable.sol"), "").unwrap();
562
563 let cwd = dir_path.join("src");
565
566 assert_eq!(
567 normalize_solidity_import_path(&cwd, "./common/Burnable.sol".as_ref()).unwrap(),
568 dir_path.join("src/common/Burnable.sol"),
569 );
570
571 assert!(normalize_solidity_import_path(&cwd, "./common/Pausable.sol".as_ref()).is_err());
572 }
573
574 #[test]
577 #[cfg(unix)]
578 fn can_normalize_solidity_import_path_symlink() {
579 let dir = tempfile::tempdir().unwrap();
580 let dir_path = dir.path();
581
582 fs::create_dir_all(dir_path.join("project/src")).unwrap();
594 fs::write(dir_path.join("project/src/Token.sol"), "").unwrap();
595 fs::create_dir(dir_path.join("project/node_modules")).unwrap();
596
597 fs::create_dir(dir_path.join("dependency")).unwrap();
598 fs::write(dir_path.join("dependency/Math.sol"), "").unwrap();
599
600 std::os::unix::fs::symlink(
601 dir_path.join("dependency"),
602 dir_path.join("project/node_modules/dependency"),
603 )
604 .unwrap();
605
606 let cwd = dir_path.join("project/src");
608
609 assert_eq!(
610 normalize_solidity_import_path(&cwd, "../node_modules/dependency/Math.sol".as_ref())
611 .unwrap(),
612 dir_path.join("project/node_modules/dependency/Math.sol"),
613 );
614 }
615
616 #[test]
617 fn can_clean_solidity_path() {
618 let clean_solidity_path = |s: &str| clean_solidity_path(s.as_ref());
619 assert_eq!(clean_solidity_path("a"), PathBuf::from("a"));
620 assert_eq!(clean_solidity_path("./a"), PathBuf::from("a"));
621 assert_eq!(clean_solidity_path("../a"), PathBuf::from("../a"));
622 assert_eq!(clean_solidity_path("/a/"), PathBuf::from("/a"));
623 assert_eq!(clean_solidity_path("//a"), PathBuf::from("/a"));
624 assert_eq!(clean_solidity_path("a/b"), PathBuf::from("a/b"));
625 assert_eq!(clean_solidity_path("a//b"), PathBuf::from("a/b"));
626 assert_eq!(clean_solidity_path("/a/b"), PathBuf::from("/a/b"));
627 assert_eq!(clean_solidity_path("a/./b"), PathBuf::from("a/b"));
628 assert_eq!(clean_solidity_path("a/././b"), PathBuf::from("a/b"));
629 assert_eq!(clean_solidity_path("/a/../b"), PathBuf::from("/b"));
630 assert_eq!(clean_solidity_path("a/./../b/."), PathBuf::from("b"));
631 assert_eq!(clean_solidity_path("a/b/c"), PathBuf::from("a/b/c"));
632 assert_eq!(clean_solidity_path("a/b/../c"), PathBuf::from("a/c"));
633 assert_eq!(clean_solidity_path("a/b/../../c"), PathBuf::from("c"));
634 assert_eq!(clean_solidity_path("a/b/../../../c"), PathBuf::from("../c"));
635 assert_eq!(
636 clean_solidity_path("a/../b/../../c/./Token.sol"),
637 PathBuf::from("../c/Token.sol")
638 );
639 }
640
641 #[test]
642 fn can_find_ancestor() {
643 let a = Path::new("/foo/bar/bar/test.txt");
644 let b = Path::new("/foo/bar/foo/example/constract.sol");
645 let expected = Path::new("/foo/bar");
646 assert_eq!(common_ancestor(a, b).unwrap(), expected.to_path_buf())
647 }
648
649 #[test]
650 fn no_common_ancestor_path() {
651 let a = Path::new("/foo/bar");
652 let b = Path::new("./bar/foo");
653 assert!(common_ancestor(a, b).is_none());
654 }
655
656 #[test]
657 fn can_find_all_ancestor() {
658 let a = Path::new("/foo/bar/foo/example.txt");
659 let b = Path::new("/foo/bar/foo/test.txt");
660 let c = Path::new("/foo/bar/bar/foo/bar");
661 let expected = Path::new("/foo/bar");
662 let paths = vec![a, b, c];
663 assert_eq!(common_ancestor_all(paths).unwrap(), expected.to_path_buf())
664 }
665}