diff --git a/ethers-solc/src/compile.rs b/ethers-solc/src/compile.rs index 9d4d7a1e..44edc409 100644 --- a/ethers-solc/src/compile.rs +++ b/ethers-solc/src/compile.rs @@ -63,7 +63,12 @@ pub static RELEASES: Lazy> = Lazy::new(|| { /// /// Supports sync and async functions. #[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)] -pub struct Solc(pub PathBuf); +pub struct Solc { + /// Path to the `solc` executable + pub solc: PathBuf, + /// Additional arguments passed to the `solc` exectuable + pub args: Vec, +} impl Default for Solc { fn default() -> Self { @@ -74,7 +79,25 @@ impl Default for Solc { impl Solc { /// A new instance which points to `solc` pub fn new(path: impl Into) -> Self { - Solc(path.into()) + Solc { solc: path.into(), args: Vec::new() } + } + + /// Adds an argument to pass to the `solc` command. + pub fn arg>(mut self, arg: T) -> Self { + self.args.push(arg.into()); + self + } + + /// Adds multiple arguments to pass to the `solc`. + pub fn args(mut self, args: I) -> Self + where + I: IntoIterator, + S: Into, + { + for arg in args { + self = self.arg(arg); + } + self } /// Returns the directory in which [svm](https://github.com/roynalnaruto/svm-rs) stores all versions @@ -112,7 +135,7 @@ impl Solc { Ok(solc) } - /// Assuming the `versions` array is sorted, it returns the latest element which satisfies + /// Assuming the `versions` array is sorted, it returns the first element which satisfies /// the provided [`VersionReq`] pub fn find_matching_installation( versions: &[Version], @@ -233,7 +256,10 @@ impl Solc { } pub fn compile_output(&self, input: &T) -> Result> { - let mut child = Command::new(&self.0) + let mut cmd = Command::new(&self.solc); + + let mut child = cmd + .args(&self.args) .arg("--standard-json") .stdin(Stdio::piped()) .stderr(Stdio::piped()) @@ -248,7 +274,7 @@ impl Solc { /// Returns the version from the configured `solc` pub fn version(&self) -> Result { version_from_output( - Command::new(&self.0) + Command::new(&self.solc) .arg("--version") .stdin(Stdio::piped()) .stderr(Stdio::piped()) @@ -288,7 +314,7 @@ impl Solc { pub async fn async_compile_output(&self, input: &T) -> Result> { use tokio::io::AsyncWriteExt; let content = serde_json::to_vec(input)?; - let mut child = tokio::process::Command::new(&self.0) + let mut child = tokio::process::Command::new(&self.solc) .arg("--standard-json") .stdin(Stdio::piped()) .stderr(Stdio::piped()) @@ -302,7 +328,7 @@ impl Solc { pub async fn async_version(&self) -> Result { version_from_output( - tokio::process::Command::new(&self.0) + tokio::process::Command::new(&self.solc) .arg("--version") .stdin(Stdio::piped()) .stderr(Stdio::piped()) @@ -338,13 +364,13 @@ fn version_from_output(output: Output) -> Result { impl AsRef for Solc { fn as_ref(&self) -> &Path { - &self.0 + &self.solc } } impl> From for Solc { fn from(solc: T) -> Self { - Solc(solc.into()) + Solc::new(solc.into()) } } @@ -451,7 +477,7 @@ mod tests { } let res = Solc::find_svm_installed_version(&version.to_string()).unwrap().unwrap(); let expected = svm::SVM_HOME.join(ver).join(format!("solc-{}", ver)); - assert_eq!(res.0, expected); + assert_eq!(res.solc, expected); } #[test] diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs index 744c9678..d6a22c03 100644 --- a/ethers-solc/src/config.rs +++ b/ethers-solc/src/config.rs @@ -272,3 +272,46 @@ impl fmt::Debug for ArtifactOutput { } } } + +use std::convert::TryFrom; + +/// Helper struct for serializing `--allow-paths` arguments to Solc +/// +/// From the [Solc docs](https://docs.soliditylang.org/en/v0.8.9/using-the-compiler.html#base-path-and-import-remapping): +/// For security reasons the compiler has restrictions on what directories it can access. +/// Directories of source files specified on the command line and target paths of +/// remappings are automatically allowed to be accessed by the file reader, +/// but everything else is rejected by default. Additional paths (and their subdirectories) +/// can be allowed via the --allow-paths /sample/path,/another/sample/path switch. +/// Everything inside the path specified via --base-path is always allowed. +#[derive(Clone, Debug, Default)] +pub struct AllowedLibPaths(pub(crate) Vec); + +impl fmt::Display for AllowedLibPaths { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let lib_paths = self + .0 + .iter() + .filter(|path| path.exists()) + .map(|path| format!("{}", path.display())) + .collect::>() + .join(","); + write!(f, "{}", lib_paths) + } +} + +impl> TryFrom> for AllowedLibPaths { + type Error = std::io::Error; + + fn try_from(libs: Vec) -> std::result::Result { + let libs = libs + .into_iter() + .map(|lib| { + let path: PathBuf = lib.into(); + let lib = std::fs::canonicalize(path)?; + Ok(lib) + }) + .collect::, std::io::Error>>()?; + Ok(AllowedLibPaths(libs)) + } +} diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs index eb5d03f8..0446d107 100644 --- a/ethers-solc/src/lib.rs +++ b/ethers-solc/src/lib.rs @@ -11,7 +11,7 @@ mod compile; pub use compile::*; mod config; -pub use config::{ArtifactOutput, ProjectPathsConfig, SolcConfig}; +pub use config::{AllowedLibPaths, ArtifactOutput, ProjectPathsConfig, SolcConfig}; use crate::{artifacts::Source, cache::SolFilesCache}; @@ -21,6 +21,7 @@ use crate::artifacts::Sources; use error::Result; use std::{ collections::{BTreeMap, HashMap}, + convert::TryInto, fmt, fs, io, path::PathBuf, }; @@ -40,6 +41,8 @@ pub struct Project { pub artifacts: ArtifactOutput, /// Errors/Warnings which match these error codes are not going to be logged pub ignored_error_codes: Vec, + /// The paths which will be allowed for library inclusion + pub allowed_lib_paths: AllowedLibPaths, } impl Project { @@ -116,8 +119,12 @@ impl Project { let version = Solc::detect_version(&source)?; // gets the solc binary for that version, it is expected tha this will succeed // AND find the solc since it was installed right above - let solc = Solc::find_svm_installed_version(version.to_string())? + let mut solc = Solc::find_svm_installed_version(version.to_string())? .expect("solc should have been installed"); + + if !self.allowed_lib_paths.0.is_empty() { + solc = solc.arg("--allow-paths").arg(self.allowed_lib_paths.to_string()); + } let entry = sources_by_version.entry(solc).or_insert_with(BTreeMap::new); entry.insert(path, source); } @@ -132,7 +139,6 @@ impl Project { res.contracts.extend(compiled.contracts); } } - Ok(if res.contracts.is_empty() { ProjectCompileOutput::Unchanged } else { @@ -214,6 +220,8 @@ pub struct ProjectBuilder { artifacts: Option, /// Which error codes to ignore pub ignored_error_codes: Vec, + /// All allowed paths + pub allowed_paths: Vec, } impl ProjectBuilder { @@ -248,8 +256,34 @@ impl ProjectBuilder { self } + /// Adds an allowed-path to the solc executable + pub fn allowed_path>(mut self, path: T) -> Self { + self.allowed_paths.push(path.into()); + self + } + + /// Adds multiple allowed-path to the solc executable + pub fn allowed_paths(mut self, args: I) -> Self + where + I: IntoIterator, + S: Into, + { + for arg in args { + self = self.allowed_path(arg); + } + self + } + pub fn build(self) -> Result { - let Self { paths, solc, solc_config, cached, artifacts, ignored_error_codes } = self; + let Self { + paths, + solc, + solc_config, + cached, + artifacts, + ignored_error_codes, + mut allowed_paths, + } = self; let solc = solc.unwrap_or_default(); let solc_config = solc_config.map(Ok).unwrap_or_else(|| { @@ -257,13 +291,21 @@ impl ProjectBuilder { SolcConfig::builder().version(version.to_string()).build() })?; + let paths = paths.map(Ok).unwrap_or_else(ProjectPathsConfig::current_hardhat)?; + + if allowed_paths.is_empty() { + // allow every contract under root by default + allowed_paths.push(paths.root.clone()) + } + Ok(Project { - paths: paths.map(Ok).unwrap_or_else(ProjectPathsConfig::current_hardhat)?, + paths, solc, solc_config, cached, artifacts: artifacts.unwrap_or_default(), ignored_error_codes, + allowed_lib_paths: allowed_paths.try_into()?, }) } } @@ -277,6 +319,7 @@ impl Default for ProjectBuilder { cached: true, artifacts: None, ignored_error_codes: Vec::new(), + allowed_paths: vec![], } } } @@ -329,4 +372,35 @@ mod tests { // Contracts A to F assert_eq!(contracts.keys().count(), 5); } + + #[test] + #[cfg(all(feature = "svm", feature = "async"))] + fn test_build_many_libs() { + use super::*; + + let root = std::fs::canonicalize("./test-data/test-contract-libs").unwrap(); + + let paths = ProjectPathsConfig::builder() + .root(&root) + .sources(root.join("src")) + .lib(root.join("lib1")) + .lib(root.join("lib2")) + .build() + .unwrap(); + let project = Project::builder() + .paths(paths) + .ephemeral() + .artifacts(ArtifactOutput::Nothing) + .build() + .unwrap(); + let compiled = project.compile().unwrap(); + let contracts = match compiled { + ProjectCompileOutput::Compiled((out, _)) => { + assert!(!out.has_error()); + out.contracts + } + _ => panic!("must compile"), + }; + assert_eq!(contracts.keys().count(), 3); + } } diff --git a/ethers-solc/test-data/test-contract-libs/lib1/Bar.sol b/ethers-solc/test-data/test-contract-libs/lib1/Bar.sol new file mode 100644 index 00000000..16dfcecb --- /dev/null +++ b/ethers-solc/test-data/test-contract-libs/lib1/Bar.sol @@ -0,0 +1,3 @@ +pragma solidity ^0.8.6; + +contract Bar {} diff --git a/ethers-solc/test-data/test-contract-libs/lib2/Baz.sol b/ethers-solc/test-data/test-contract-libs/lib2/Baz.sol new file mode 100644 index 00000000..e4527d07 --- /dev/null +++ b/ethers-solc/test-data/test-contract-libs/lib2/Baz.sol @@ -0,0 +1,3 @@ +pragma solidity ^0.8.6; + +contract Baz {} diff --git a/ethers-solc/test-data/test-contract-libs/src/Foo.sol b/ethers-solc/test-data/test-contract-libs/src/Foo.sol new file mode 100644 index 00000000..ab9eba94 --- /dev/null +++ b/ethers-solc/test-data/test-contract-libs/src/Foo.sol @@ -0,0 +1,6 @@ +pragma solidity 0.8.6; + +import "../lib1/Bar.sol"; +import "../lib2/Baz.sol"; + +contract Foo is Bar, Baz {}