2021-10-26 11:28:10 +00:00
//! Utility functions
2021-10-30 17:59:44 +00:00
use std ::path ::{ Component , Path , PathBuf } ;
2021-10-26 11:28:10 +00:00
2021-12-04 17:13:58 +00:00
use crate ::error ::SolcError ;
2021-10-26 11:28:10 +00:00
use once_cell ::sync ::Lazy ;
use regex ::Regex ;
2021-12-04 17:13:58 +00:00
use semver ::Version ;
2021-12-08 00:38:29 +00:00
use tiny_keccak ::{ Hasher , Keccak } ;
2021-10-26 11:28:10 +00:00
use walkdir ::WalkDir ;
/// A regex that matches the import path and identifier of a solidity import
/// statement with the named groups "path", "id".
pub static RE_SOL_IMPORT : Lazy < Regex > = Lazy ::new ( | | {
// Adapted from https://github.com/nomiclabs/hardhat/blob/cced766c65b25d3d0beb39ef847246ac9618bdd9/packages/hardhat-core/src/internal/solidity/parse.ts#L100
2021-11-29 21:45:07 +00:00
Regex ::new ( r # "import\s+(?:(?:"(?P<p1>[^;]*)"|'([^;]*)')(?:;|\s+as\s+(?P<id>[^;]*);)|.+from\s+(?:"(?P<p2>.*)"|'(?P<p3>.*)');)"# ) . unwrap ( )
2021-10-26 11:28:10 +00:00
} ) ;
/// A regex that matches the version part of a solidity pragma
/// as follows: `pragma solidity ^0.5.2;` => `^0.5.2`
/// statement with the named groups "path", "id".
// Adapted from https://github.com/nomiclabs/hardhat/blob/cced766c65b25d3d0beb39ef847246ac9618bdd9/packages/hardhat-core/src/internal/solidity/parse.ts#L119
pub static RE_SOL_PRAGMA_VERSION : Lazy < Regex > =
Lazy ::new ( | | Regex ::new ( r "pragma\s+solidity\s+(?P<version>.+?);" ) . unwrap ( ) ) ;
/// Returns all path parts from any solidity import statement in a string,
/// `import "./contracts/Contract.sol";` -> `"./contracts/Contract.sol"`.
///
/// See also https://docs.soliditylang.org/en/v0.8.9/grammar.html
pub fn find_import_paths ( contract : & str ) -> Vec < & str > {
RE_SOL_IMPORT
. captures_iter ( contract )
2021-11-29 21:45:07 +00:00
. filter_map ( | cap | cap . name ( " p1 " ) . or_else ( | | cap . name ( " p2 " ) ) . or_else ( | | cap . name ( " p3 " ) ) )
2021-10-26 11:28:10 +00:00
. map ( | m | m . as_str ( ) )
. collect ( )
}
/// Returns the solidity version pragma from the given input:
/// `pragma solidity ^0.5.2;` => `^0.5.2`
pub fn find_version_pragma ( contract : & str ) -> Option < & str > {
2021-10-29 12:29:35 +00:00
RE_SOL_PRAGMA_VERSION . captures ( contract ) ? . name ( " version " ) . map ( | m | m . as_str ( ) )
2021-10-26 11:28:10 +00:00
}
/// Returns a list of absolute paths to all the solidity files under the root
///
/// NOTE: this does not resolve imports from other locations
///
/// # Example
///
/// ```no_run
/// use ethers_solc::utils;
/// let sources = utils::source_files("./contracts").unwrap();
/// ```
pub fn source_files ( root : impl AsRef < Path > ) -> walkdir ::Result < Vec < PathBuf > > {
let files = WalkDir ::new ( root )
. into_iter ( )
. filter_map ( Result ::ok )
. filter ( | e | e . file_type ( ) . is_file ( ) )
2021-10-29 12:29:35 +00:00
. filter ( | e | e . path ( ) . extension ( ) . map ( | ext | ext = = " sol " ) . unwrap_or_default ( ) )
2021-10-26 11:28:10 +00:00
. map ( | e | e . path ( ) . into ( ) )
. collect ( ) ;
Ok ( files )
}
2021-10-30 17:59:44 +00:00
/// Returns the source name for the given source path, the ancestors of the root path
/// `/Users/project/sources/contract.sol` -> `sources/contracts.sol`
pub fn source_name ( source : & Path , root : impl AsRef < Path > ) -> & Path {
source . strip_prefix ( root . as_ref ( ) ) . unwrap_or ( source )
}
/// Attempts to determine if the given source is a local, relative import
pub fn is_local_source_name ( libs : & [ impl AsRef < Path > ] , source : impl AsRef < Path > ) -> bool {
resolve_library ( libs , source ) . is_none ( )
}
/// Returns the path to the library if the source path is in fact determined to be a library path,
/// and it exists.
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 ) = > {
// attempt to verify that the root component of this source exists under a library
// folder
for lib in libs {
let lib = lib . as_ref ( ) ;
let contract = lib . join ( source ) ;
if contract . exists ( ) {
// contract exists in <lib>/<source>
return Some ( contract )
}
// check for <lib>/<first_dir>/src/name.sol
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 ,
}
}
2021-12-04 17:13:58 +00:00
/// Reads the list of Solc versions that have been installed in the machine. The version list is
/// sorted in ascending order.
/// Checks for installed solc versions under the given path as
/// `<root>/<major.minor.path>`, (e.g.: `~/.svm/0.8.10`)
/// and returns them sorted in ascending order
pub fn installed_versions ( root : impl AsRef < Path > ) -> Result < Vec < Version > , SolcError > {
let mut versions : Vec < _ > = walkdir ::WalkDir ::new ( root )
. max_depth ( 1 )
. into_iter ( )
. filter_map ( std ::result ::Result ::ok )
. filter ( | e | e . file_type ( ) . is_dir ( ) )
. filter_map ( | e : walkdir ::DirEntry | {
e . path ( ) . file_name ( ) . and_then ( | v | Version ::parse ( v . to_string_lossy ( ) . as_ref ( ) ) . ok ( ) )
} )
. collect ( ) ;
versions . sort ( ) ;
Ok ( versions )
}
2021-12-08 00:38:29 +00:00
/// Returns the 36 char (deprecated) fully qualified name placeholder
///
/// If the name is longer than 36 char, then the name gets truncated,
/// If the name is shorter than 36 char, then the name is filled with trailing `_`
pub fn library_fully_qualified_placeholder ( name : impl AsRef < str > ) -> String {
name . as_ref ( ) . chars ( ) . chain ( std ::iter ::repeat ( '_' ) ) . take ( 36 ) . collect ( )
}
/// Returns the library hash placeholder as `$hex(library_hash(name))$`
pub fn library_hash_placeholder ( name : impl AsRef < [ u8 ] > ) -> String {
let hash = library_hash ( name ) ;
let placeholder = hex ::encode ( hash ) ;
format! ( " $ {} $ " , placeholder )
}
/// Returns the library placeholder for the given name
/// The placeholder is a 34 character prefix of the hex encoding of the keccak256 hash of the fully
/// qualified library name.
///
/// See also https://docs.soliditylang.org/en/develop/using-the-compiler.html#library-linking
pub fn library_hash ( name : impl AsRef < [ u8 ] > ) -> [ u8 ; 17 ] {
let mut output = [ 0 u8 ; 17 ] ;
let mut hasher = Keccak ::v256 ( ) ;
hasher . update ( name . as_ref ( ) ) ;
hasher . finalize ( & mut output ) ;
output
}
2021-10-26 11:28:10 +00:00
#[ cfg(test) ]
mod tests {
2021-10-30 17:59:44 +00:00
use super ::* ;
2021-10-26 11:28:10 +00:00
use std ::{
collections ::HashSet ,
fs ::{ create_dir_all , File } ,
} ;
use tempdir ::TempDir ;
2021-10-30 17:59:44 +00:00
#[ 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 ::new ( " 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 " ) ) ;
}
2021-10-26 11:28:10 +00:00
#[ test ]
fn can_find_solidity_sources ( ) {
let tmp_dir = TempDir ::new ( " contracts " ) . unwrap ( ) ;
let file_a = tmp_dir . path ( ) . join ( " a.sol " ) ;
let file_b = tmp_dir . path ( ) . join ( " a.sol " ) ;
let nested = tmp_dir . path ( ) . join ( " nested " ) ;
let file_c = nested . join ( " c.sol " ) ;
let nested_deep = nested . join ( " deep " ) ;
let file_d = nested_deep . join ( " d.sol " ) ;
File ::create ( & file_a ) . unwrap ( ) ;
File ::create ( & file_b ) . unwrap ( ) ;
create_dir_all ( nested_deep ) . unwrap ( ) ;
File ::create ( & file_c ) . unwrap ( ) ;
File ::create ( & file_d ) . unwrap ( ) ;
let files : HashSet < _ > = source_files ( tmp_dir . path ( ) ) . unwrap ( ) . into_iter ( ) . collect ( ) ;
let expected : HashSet < _ > = [ file_a , file_b , file_c , file_d ] . into ( ) ;
assert_eq! ( files , expected ) ;
}
#[ test ]
fn can_find_import_paths ( ) {
let s = r ##" //SPDX-License-Identifier: Unlicense
pragma solidity ^ 0. 8.0 ;
import " hardhat/console.sol " ;
import " ../contract/Contract.sol " ;
2021-11-29 21:45:07 +00:00
import { T } from " ../Test.sol " ;
import { T } from ' .. / Test2 . sol ' ;
2021-10-26 11:28:10 +00:00
" ##;
2021-11-29 21:45:07 +00:00
assert_eq! (
vec! [ " hardhat/console.sol " , " ../contract/Contract.sol " , " ../Test.sol " , " ../Test2.sol " ] ,
find_import_paths ( s )
) ;
2021-10-26 11:28:10 +00:00
}
#[ test ]
fn can_find_version ( ) {
let s = r ##" //SPDX-License-Identifier: Unlicense
pragma solidity ^ 0. 8.0 ;
" ##;
assert_eq! ( Some ( " ^0.8.0 " ) , find_version_pragma ( s ) ) ;
}
}