-
Notifications
You must be signed in to change notification settings - Fork 52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: create verifiable Standard JSON for projects with external files #36
Changes from 3 commits
1d48253
e0e1bfb
5c781d0
38eff70
bf564fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -490,8 +490,6 @@ impl<T: ArtifactOutput> Project<T> { | |
&self, | ||
target: impl AsRef<Path>, | ||
) -> Result<StandardJsonCompilerInput> { | ||
use path_slash::PathExt; | ||
|
||
let target = target.as_ref(); | ||
tracing::trace!("Building standard-json-input for {:?}", target); | ||
let graph = Graph::resolve(&self.paths)?; | ||
|
@@ -514,14 +512,7 @@ impl<T: ArtifactOutput> Project<T> { | |
let root = self.root(); | ||
let sources = sources | ||
.into_iter() | ||
.map(|(path, source)| { | ||
let path: PathBuf = if let Ok(stripped) = path.strip_prefix(root) { | ||
stripped.to_slash_lossy().into_owned().into() | ||
} else { | ||
path.to_slash_lossy().into_owned().into() | ||
}; | ||
(path, source.clone()) | ||
}) | ||
.map(|(path, source)| (rebase_path(root, path), source.clone())) | ||
.collect(); | ||
|
||
let mut settings = self.solc_config.settings.clone(); | ||
|
@@ -954,6 +945,43 @@ impl<T: ArtifactOutput> ArtifactOutput for Project<T> { | |
} | ||
} | ||
|
||
// Rebases the given path to the base directory lexically. | ||
// | ||
// The returned path from this function usually starts either with a normal component (e.g., `src`) | ||
// or a parent directory component (i.e., `..`), which is based on the base directory. Additionally, | ||
// this function converts the path into a UTF-8 string and replaces all separators with forward | ||
// slashes (`/`). | ||
// | ||
// The rebasing process is as follows: | ||
// | ||
// 1. Remove the leading components from the path that match the base components. | ||
// 2. Prepend `..` components to the path, equal in number to the remaining base components. | ||
fn rebase_path(base: impl AsRef<Path>, path: impl AsRef<Path>) -> PathBuf { | ||
use path_slash::PathExt; | ||
|
||
let base = base.as_ref(); | ||
let path = path.as_ref(); | ||
|
||
let mut new_path = path.components().collect::<std::collections::VecDeque<_>>(); | ||
|
||
for (i, (base_component, path_component)) in | ||
base.components().zip(path.components()).enumerate() | ||
{ | ||
if base_component == path_component { | ||
new_path.pop_front(); | ||
} else { | ||
let mut parent_dirs = | ||
vec![std::path::Component::ParentDir; base.components().count() - i]; | ||
parent_dirs.extend(new_path); | ||
new_path = parent_dirs.into(); | ||
|
||
break; | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can solve this a bit differently this way we don't need the vecdeque There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I'm not sure I understand your suggestion. If we compare the components of base and path at the same positions, it seems similar to using zip. Could you please elaborate on the type of implementation you have in mind? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. something like let mut components = Vec::new(); // perhaps we can even directly push components to a PathBuf?
let mut base_iter = base.components();
let mut path_iter = path.components();
while let Some(path) = path_iter.next() {
if Some(path) != base_iter.next() {
components.push(path);
components.extend(path_iter);
}
}
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for your explanation. I now understand your point. I've made the change and pushed it. |
||
|
||
new_path.iter().collect::<PathBuf>().to_slash_lossy().into_owned().into() | ||
} | ||
|
||
#[cfg(test)] | ||
#[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))] | ||
mod tests { | ||
|
@@ -1014,4 +1042,43 @@ mod tests { | |
let contracts = project.compile().unwrap().succeeded().output().contracts; | ||
assert_eq!(contracts.contracts().count(), 2); | ||
} | ||
|
||
#[test] | ||
fn can_rebase_path() { | ||
assert_eq!(rebase_path("a/b", "a/b/c"), PathBuf::from("c")); | ||
assert_eq!(rebase_path("a/b", "a/c"), PathBuf::from("../c")); | ||
assert_eq!(rebase_path("a/b", "c"), PathBuf::from("../../c")); | ||
|
||
assert_eq!( | ||
rebase_path("/home/user/project", "/home/user/project/A.sol"), | ||
PathBuf::from("A.sol") | ||
); | ||
assert_eq!( | ||
rebase_path("/home/user/project", "/home/user/project/src/A.sol"), | ||
PathBuf::from("src/A.sol") | ||
); | ||
assert_eq!( | ||
rebase_path("/home/user/project", "/home/user/project/lib/forge-std/src/Test.sol"), | ||
PathBuf::from("lib/forge-std/src/Test.sol") | ||
); | ||
assert_eq!( | ||
rebase_path("/home/user/project", "/home/user/A.sol"), | ||
PathBuf::from("../A.sol") | ||
); | ||
assert_eq!(rebase_path("/home/user/project", "/home/A.sol"), PathBuf::from("../../A.sol")); | ||
assert_eq!(rebase_path("/home/user/project", "/A.sol"), PathBuf::from("../../../A.sol")); | ||
assert_eq!( | ||
rebase_path("/home/user/project", "/tmp/A.sol"), | ||
PathBuf::from("../../../tmp/A.sol") | ||
); | ||
|
||
assert_eq!( | ||
rebase_path("/Users/ah/temp/verif", "/Users/ah/temp/remapped/Child.sol"), | ||
PathBuf::from("../remapped/Child.sol") | ||
); | ||
assert_eq!( | ||
rebase_path("/Users/ah/temp/verif", "/Users/ah/temp/verif/../remapped/Parent.sol"), | ||
PathBuf::from("../remapped/Parent.sol") | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,7 +12,7 @@ use foundry_compilers::{ | |
info::ContractInfo, | ||
project_util::*, | ||
remappings::Remapping, | ||
Artifact, CompilerInput, ConfigurableArtifacts, ExtraOutputValues, Graph, Project, | ||
utils, Artifact, CompilerInput, ConfigurableArtifacts, ExtraOutputValues, Graph, Project, | ||
ProjectCompileOutput, ProjectPathsConfig, Solc, TestFileFilter, | ||
}; | ||
use pretty_assertions::assert_eq; | ||
|
@@ -1600,6 +1600,71 @@ fn can_sanitize_bytecode_hash() { | |
assert!(compiled.find_first("A").is_some()); | ||
} | ||
|
||
// https://github.com/foundry-rs/foundry/issues/5307 | ||
#[test] | ||
fn can_create_standard_json_input_with_external_file() { | ||
// File structure: | ||
// . | ||
// ├── verif | ||
// │ └── src | ||
// │ └── Counter.sol | ||
// └── remapped | ||
// ├── Child.sol | ||
// └── Parent.sol | ||
|
||
let dir = tempfile::tempdir().unwrap(); | ||
let verif_dir = utils::canonicalize(dir.path()).unwrap().join("verif"); | ||
let remapped_dir = utils::canonicalize(dir.path()).unwrap().join("remapped"); | ||
Comment on lines
+1616
to
+1617
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After my previous PR is merged, this canonicalization can be removed. It is necessary on this branch because the symlink-resolving canonicalization is still in effect, and the temporary directory on macOS is a symlink. |
||
fs::create_dir_all(verif_dir.join("src")).unwrap(); | ||
fs::create_dir(&remapped_dir).unwrap(); | ||
|
||
let mut verif_project = Project::builder() | ||
.paths(ProjectPathsConfig::dapptools(&verif_dir).unwrap()) | ||
.build() | ||
.unwrap(); | ||
|
||
verif_project.paths.remappings.push(Remapping { | ||
context: None, | ||
name: "@remapped/".into(), | ||
path: "../remapped/".into(), | ||
}); | ||
verif_project.allowed_paths.insert(remapped_dir.clone()); | ||
|
||
fs::write(remapped_dir.join("Parent.sol"), "pragma solidity >=0.8.0; import './Child.sol';") | ||
.unwrap(); | ||
fs::write(remapped_dir.join("Child.sol"), "pragma solidity >=0.8.0;").unwrap(); | ||
fs::write( | ||
verif_dir.join("src/Counter.sol"), | ||
"pragma solidity >=0.8.0; import '@remapped/Parent.sol'; contract Counter {}", | ||
) | ||
.unwrap(); | ||
|
||
// solc compiles using the host file system; therefore, this setup is considered valid | ||
let compiled = verif_project.compile().unwrap(); | ||
compiled.assert_success(); | ||
|
||
// can create project root based paths | ||
let std_json = verif_project.standard_json_input(verif_dir.join("src/Counter.sol")).unwrap(); | ||
assert_eq!( | ||
std_json.sources.iter().map(|(path, _)| path.clone()).collect::<Vec<_>>(), | ||
vec![ | ||
PathBuf::from("src/Counter.sol"), | ||
PathBuf::from("../remapped/Parent.sol"), | ||
PathBuf::from("../remapped/Child.sol") | ||
] | ||
); | ||
|
||
// can compile using the created json | ||
let compiler_errors = Solc::default() | ||
.compile(&std_json) | ||
.unwrap() | ||
.errors | ||
.into_iter() | ||
.filter_map(|e| if e.severity.is_error() { Some(e.message) } else { None }) | ||
.collect::<Vec<_>>(); | ||
assert!(compiler_errors.is_empty(), "{:?}", compiler_errors); | ||
} | ||
|
||
#[test] | ||
fn can_compile_std_json_input() { | ||
let tmp = TempProject::dapptools_init().unwrap(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we add an example here that illustrates this, otherwise this is a bit hard to understand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated the comment, adding examples to make it easier to understand.