use crate::error::{SolcError, SolcIoError};
use alloy_primitives::{hex, keccak256};
use semver::{Version, VersionReq};
use serde::{Serialize, de::DeserializeOwned};
use std::{
fs,
io::Write,
ops::Range,
path::{Component, Path, PathBuf},
sync::LazyLock as Lazy,
};
#[cfg(feature = "regex")]
mod re;
#[cfg(feature = "regex")]
pub use re::*;
#[cfg(feature = "walkdir")]
mod wd;
#[cfg(feature = "walkdir")]
pub use wd::*;
pub const SOLC_EXTENSIONS: &[&str] = &["sol", "yul"];
pub const BYZANTIUM_SOLC: Version = Version::new(0, 4, 21);
pub const CONSTANTINOPLE_SOLC: Version = Version::new(0, 4, 22);
pub const PETERSBURG_SOLC: Version = Version::new(0, 5, 5);
pub const ISTANBUL_SOLC: Version = Version::new(0, 5, 14);
pub const BERLIN_SOLC: Version = Version::new(0, 8, 5);
pub const LONDON_SOLC: Version = Version::new(0, 8, 7);
pub const PARIS_SOLC: Version = Version::new(0, 8, 18);
pub const SHANGHAI_SOLC: Version = Version::new(0, 8, 20);
pub const CANCUN_SOLC: Version = Version::new(0, 8, 24);
pub const PRAGUE_SOLC: Version = Version::new(0, 8, 27);
pub const OSAKA_SOLC: Version = Version::new(0, 8, 29);
pub static SUPPORTS_BASE_PATH: Lazy<VersionReq> =
Lazy::new(|| VersionReq::parse(">=0.6.9").unwrap());
pub static SUPPORTS_INCLUDE_PATH: Lazy<VersionReq> =
Lazy::new(|| VersionReq::parse(">=0.8.8").unwrap());
#[cfg(feature = "hasher")]
pub fn unique_hash(input: impl AsRef<[u8]>) -> String {
encode_hash(xxhash_rust::xxh3::xxh3_64(input.as_ref()))
}
#[cfg(feature = "hasher")]
pub fn unique_hash_many(inputs: impl IntoIterator<Item = impl AsRef<[u8]>>) -> String {
let mut hasher = xxhash_rust::xxh3::Xxh3Default::new();
for input in inputs {
hasher.update(input.as_ref());
}
encode_hash(hasher.digest())
}
#[cfg(feature = "hasher")]
fn encode_hash(x: u64) -> String {
hex::encode(x.to_be_bytes())
}
pub const fn range_by_offset(range: &Range<usize>, offset: isize) -> Range<usize> {
Range {
start: offset.saturating_add(range.start as isize) as usize,
end: offset.saturating_add(range.end as isize) as usize,
}
}
pub fn source_name<'a>(source: &'a Path, root: &Path) -> &'a Path {
strip_prefix(source, root)
}
pub fn strip_prefix<'a>(source: &'a Path, root: &Path) -> &'a Path {
source.strip_prefix(root).unwrap_or(source)
}
pub fn strip_prefix_owned(source: PathBuf, root: &Path) -> PathBuf {
source.strip_prefix(root).map(Path::to_path_buf).unwrap_or(source)
}
pub fn is_local_source_name(libs: &[impl AsRef<Path>], source: impl AsRef<Path>) -> bool {
resolve_library(libs, source.as_ref()).is_none()
}
pub fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf, SolcIoError> {
let path = path.as_ref();
let res = dunce::canonicalize(path);
#[cfg(windows)]
let res = res.map(|p| {
use path_slash::PathBufExt;
PathBuf::from(p.to_slash_lossy().as_ref())
});
res.map_err(|err| SolcIoError::new(err, path))
}
pub fn normalize_solidity_import_path(
directory: &Path,
import_path: &Path,
) -> Result<PathBuf, SolcIoError> {
let original = directory.join(import_path);
let cleaned = clean_solidity_path(&original);
let normalized = dunce::simplified(&cleaned);
#[cfg(windows)]
let normalized = {
use path_slash::PathExt;
PathBuf::from(normalized.to_slash_lossy().as_ref())
};
#[cfg(not(windows))]
let normalized = PathBuf::from(normalized);
let _ = normalized.metadata().map_err(|err| SolcIoError::new(err, original))?;
Ok(normalized)
}
fn clean_solidity_path(original_path: &Path) -> PathBuf {
let mut new_path = Vec::new();
for component in original_path.components() {
match component {
Component::Prefix(..) | Component::RootDir | Component::Normal(..) => {
new_path.push(component);
}
Component::CurDir => {}
Component::ParentDir => {
if let Some(Component::Normal(..)) = new_path.last() {
new_path.pop();
} else {
new_path.push(component);
}
}
}
}
new_path.iter().collect()
}
pub fn canonicalized(path: impl Into<PathBuf>) -> PathBuf {
let path = path.into();
canonicalize(&path).unwrap_or(path)
}
pub fn resolve_library(libs: &[impl AsRef<Path>], source: impl AsRef<Path>) -> Option<PathBuf> {
let source = source.as_ref();
let comp = source.components().next()?;
match comp {
Component::Normal(first_dir) => {
for lib in libs {
let lib = lib.as_ref();
let contract = lib.join(source);
if contract.exists() {
return Some(contract);
}
let contract = lib
.join(first_dir)
.join("src")
.join(source.strip_prefix(first_dir).expect("is first component"));
if contract.exists() {
return Some(contract);
}
}
None
}
Component::RootDir => Some(source.into()),
_ => None,
}
}
pub fn resolve_absolute_library(
root: &Path,
cwd: &Path,
import: &Path,
) -> Option<(PathBuf, PathBuf)> {
let mut parent = cwd.parent()?;
while parent != root {
if let Ok(import) = normalize_solidity_import_path(parent, import) {
return Some((parent.to_path_buf(), import));
}
parent = parent.parent()?;
}
None
}
pub fn library_fully_qualified_placeholder(name: &str) -> String {
name.chars().chain(std::iter::repeat('_')).take(36).collect()
}
pub fn library_hash_placeholder(name: impl AsRef<[u8]>) -> String {
let mut s = String::with_capacity(34 + 2);
s.push('$');
s.push_str(hex::Buffer::<17, false>::new().format(&library_hash(name)));
s.push('$');
s
}
pub fn library_hash(name: impl AsRef<[u8]>) -> [u8; 17] {
let hash = keccak256(name);
hash[..17].try_into().unwrap()
}
pub fn common_ancestor_all<I, P>(paths: I) -> Option<PathBuf>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
let mut iter = paths.into_iter();
let mut ret = iter.next()?.as_ref().to_path_buf();
for path in iter {
ret = common_ancestor(&ret, path.as_ref())?;
}
Some(ret)
}
pub fn common_ancestor(a: &Path, b: &Path) -> Option<PathBuf> {
let a = a.components();
let b = b.components();
let mut ret = PathBuf::new();
let mut found = false;
for (c1, c2) in a.zip(b) {
if c1 == c2 {
ret.push(c1);
found = true;
} else {
break;
}
}
found.then_some(ret)
}
pub fn find_fave_or_alt_path(root: &Path, fave: &str, alt: &str) -> PathBuf {
let p = root.join(fave);
if !p.exists() {
let alt = root.join(alt);
if alt.exists() {
return alt;
}
}
p
}
#[cfg(any(feature = "async", feature = "svm-solc"))]
use tokio::runtime::{Handle, Runtime};
#[cfg(any(feature = "async", feature = "svm-solc"))]
#[derive(Debug)]
pub enum RuntimeOrHandle {
Runtime(Runtime),
Handle(Handle),
}
#[cfg(any(feature = "async", feature = "svm-solc"))]
impl Default for RuntimeOrHandle {
fn default() -> Self {
Self::new()
}
}
#[cfg(any(feature = "async", feature = "svm-solc"))]
impl RuntimeOrHandle {
pub fn new() -> Self {
match Handle::try_current() {
Ok(handle) => Self::Handle(handle),
Err(_) => Self::Runtime(Runtime::new().expect("Failed to start runtime")),
}
}
pub fn block_on<F: std::future::Future>(&self, f: F) -> F::Output {
match &self {
Self::Runtime(runtime) => runtime.block_on(f),
Self::Handle(handle) => tokio::task::block_in_place(|| handle.block_on(f)),
}
}
}
#[cfg(any(test, feature = "project-util", feature = "test-utils"))]
pub fn tempdir(name: &str) -> Result<tempfile::TempDir, SolcIoError> {
tempfile::Builder::new().prefix(name).tempdir().map_err(|err| SolcIoError::new(err, name))
}
pub fn read_json_file<T: DeserializeOwned>(path: &Path) -> Result<T, SolcError> {
let s = fs::read_to_string(path).map_err(|err| SolcError::io(err, path))?;
serde_json::from_str(&s).map_err(Into::into)
}
pub fn write_json_file<T: Serialize>(
value: &T,
path: &Path,
capacity: usize,
) -> Result<(), SolcError> {
let file = fs::File::create(path).map_err(|err| SolcError::io(err, path))?;
let mut writer = std::io::BufWriter::with_capacity(capacity, file);
serde_json::to_writer(&mut writer, value)?;
writer.flush().map_err(|e| SolcError::io(e, path))
}
pub fn create_parent_dir_all(file: &Path) -> Result<(), SolcError> {
if let Some(parent) = file.parent() {
fs::create_dir_all(parent).map_err(|err| {
SolcError::msg(format!(
"Failed to create artifact parent folder \"{}\": {}",
parent.display(),
err
))
})?;
}
Ok(())
}
#[cfg(any(test, feature = "test-utils"))]
pub fn touch(path: &std::path::Path) -> std::io::Result<()> {
match std::fs::OpenOptions::new().create(true).write(true).truncate(false).open(path) {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
}
#[cfg(any(test, feature = "test-utils"))]
pub fn mkdir_or_touch(tmp: &std::path::Path, paths: &[&str]) {
for path in paths {
if let Some(parent) = Path::new(path).parent() {
std::fs::create_dir_all(tmp.join(parent)).unwrap();
}
if path.ends_with(".sol") {
let path = tmp.join(path);
touch(&path).unwrap();
} else {
let path: PathBuf = tmp.join(path);
std::fs::create_dir_all(path).unwrap();
}
}
}
#[cfg(test)]
mod tests {
pub use super::*;
pub use std::fs::{File, create_dir_all};
#[test]
fn can_create_parent_dirs_with_ext() {
let tmp_dir = tempdir("out").unwrap();
let path = tmp_dir.path().join("IsolationModeMagic.sol/IsolationModeMagic.json");
create_parent_dir_all(&path).unwrap();
assert!(path.parent().unwrap().is_dir());
}
#[test]
fn can_create_parent_dirs_versioned() {
let tmp_dir = tempdir("out").unwrap();
let path = tmp_dir.path().join("IVersioned.sol/IVersioned.0.8.16.json");
create_parent_dir_all(&path).unwrap();
assert!(path.parent().unwrap().is_dir());
let path = tmp_dir.path().join("IVersioned.sol/IVersioned.json");
create_parent_dir_all(&path).unwrap();
assert!(path.parent().unwrap().is_dir());
}
#[test]
fn can_determine_local_paths() {
assert!(is_local_source_name(&[""], "./local/contract.sol"));
assert!(is_local_source_name(&[""], "../local/contract.sol"));
assert!(!is_local_source_name(&[""], "/ds-test/test.sol"));
let tmp_dir = tempdir("contracts").unwrap();
let dir = tmp_dir.path().join("ds-test");
create_dir_all(&dir).unwrap();
File::create(dir.join("test.sol")).unwrap();
assert!(!is_local_source_name(&[tmp_dir.path()], "ds-test/test.sol"));
}
#[test]
fn can_normalize_solidity_import_path() {
let dir = tempfile::tempdir().unwrap();
let dir_path = dir.path();
fs::create_dir_all(dir_path.join("src/common")).unwrap();
fs::write(dir_path.join("src/Token.sol"), "").unwrap();
fs::write(dir_path.join("src/common/Burnable.sol"), "").unwrap();
let cwd = dir_path.join("src");
assert_eq!(
normalize_solidity_import_path(&cwd, "./common/Burnable.sol".as_ref()).unwrap(),
dir_path.join("src/common/Burnable.sol"),
);
assert!(normalize_solidity_import_path(&cwd, "./common/Pausable.sol".as_ref()).is_err());
}
#[test]
#[cfg(unix)]
fn can_normalize_solidity_import_path_symlink() {
let dir = tempfile::tempdir().unwrap();
let dir_path = dir.path();
fs::create_dir_all(dir_path.join("project/src")).unwrap();
fs::write(dir_path.join("project/src/Token.sol"), "").unwrap();
fs::create_dir(dir_path.join("project/node_modules")).unwrap();
fs::create_dir(dir_path.join("dependency")).unwrap();
fs::write(dir_path.join("dependency/Math.sol"), "").unwrap();
std::os::unix::fs::symlink(
dir_path.join("dependency"),
dir_path.join("project/node_modules/dependency"),
)
.unwrap();
let cwd = dir_path.join("project/src");
assert_eq!(
normalize_solidity_import_path(&cwd, "../node_modules/dependency/Math.sol".as_ref())
.unwrap(),
dir_path.join("project/node_modules/dependency/Math.sol"),
);
}
#[test]
fn can_clean_solidity_path() {
let clean_solidity_path = |s: &str| clean_solidity_path(s.as_ref());
assert_eq!(clean_solidity_path("a"), PathBuf::from("a"));
assert_eq!(clean_solidity_path("./a"), PathBuf::from("a"));
assert_eq!(clean_solidity_path("../a"), PathBuf::from("../a"));
assert_eq!(clean_solidity_path("/a/"), PathBuf::from("/a"));
assert_eq!(clean_solidity_path("//a"), PathBuf::from("/a"));
assert_eq!(clean_solidity_path("a/b"), PathBuf::from("a/b"));
assert_eq!(clean_solidity_path("a//b"), PathBuf::from("a/b"));
assert_eq!(clean_solidity_path("/a/b"), PathBuf::from("/a/b"));
assert_eq!(clean_solidity_path("a/./b"), PathBuf::from("a/b"));
assert_eq!(clean_solidity_path("a/././b"), PathBuf::from("a/b"));
assert_eq!(clean_solidity_path("/a/../b"), PathBuf::from("/b"));
assert_eq!(clean_solidity_path("a/./../b/."), PathBuf::from("b"));
assert_eq!(clean_solidity_path("a/b/c"), PathBuf::from("a/b/c"));
assert_eq!(clean_solidity_path("a/b/../c"), PathBuf::from("a/c"));
assert_eq!(clean_solidity_path("a/b/../../c"), PathBuf::from("c"));
assert_eq!(clean_solidity_path("a/b/../../../c"), PathBuf::from("../c"));
assert_eq!(
clean_solidity_path("a/../b/../../c/./Token.sol"),
PathBuf::from("../c/Token.sol")
);
}
#[test]
fn can_find_ancestor() {
let a = Path::new("/foo/bar/bar/test.txt");
let b = Path::new("/foo/bar/foo/example/constract.sol");
let expected = Path::new("/foo/bar");
assert_eq!(common_ancestor(a, b).unwrap(), expected.to_path_buf())
}
#[test]
fn no_common_ancestor_path() {
let a = Path::new("/foo/bar");
let b = Path::new("./bar/foo");
assert!(common_ancestor(a, b).is_none());
}
#[test]
fn can_find_all_ancestor() {
let a = Path::new("/foo/bar/foo/example.txt");
let b = Path::new("/foo/bar/foo/test.txt");
let c = Path::new("/foo/bar/bar/foo/bar");
let expected = Path::new("/foo/bar");
let paths = vec![a, b, c];
assert_eq!(common_ancestor_all(paths).unwrap(), expected.to_path_buf())
}
}