diff --git a/src/lib.rs b/src/lib.rs index 8ae261d0..ac6decb4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -490,8 +490,6 @@ impl Project { &self, target: impl AsRef, ) -> Result { - 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 Project { 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,62 @@ impl ArtifactOutput for Project { } } +// Rebases the given path to the base directory lexically. +// +// For instance, given the base `/home/user/project` and the path `/home/user/project/src/A.sol`, +// this function returns `src/A.sol`. +// +// This function transforms a path into a form that is relative to the base directory. The returned +// path starts either with a normal component (e.g., `src`) or a parent directory component (i.e., +// `..`). It also converts the path into a UTF-8 string and replaces all separators with forward +// slashes (`/`), if they're not. +// +// The rebasing process can be conceptualized as follows: +// +// 1. Remove the leading components from the path that match those in the base. +// 2. Prepend `..` components to the path, matching the number of remaining components in the base. +// +// # Examples +// +// `rebase_path("/home/user/project", "/home/user/project/src/A.sol")` returns `src/A.sol`. The +// common part, `/home/user/project`, is removed from the path. +// +// `rebase_path("/home/user/project", "/home/user/A.sol")` returns `../A.sol`. First, the common +// part, `/home/user`, is removed, leaving `A.sol`. Next, as `project` remains in the base, `..` is +// prepended to the path. +// +// On Windows, paths like `a\b\c` are converted to `a/b/c`. +// +// For more examples, see the test. +fn rebase_path(base: impl AsRef, path: impl AsRef) -> PathBuf { + use path_slash::PathExt; + + let mut base_components = base.as_ref().components(); + let mut path_components = path.as_ref().components(); + + let mut new_path = PathBuf::new(); + + while let Some(path_component) = path_components.next() { + let base_component = base_components.next(); + + if Some(path_component) != base_component { + if base_component.is_some() { + new_path.extend( + std::iter::repeat(std::path::Component::ParentDir) + .take(base_components.count() + 1), + ); + } + + new_path.push(path_component); + new_path.extend(path_components); + + break; + } + } + + new_path.to_slash_lossy().into_owned().into() +} + #[cfg(test)] #[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))] mod tests { @@ -1014,4 +1061,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") + ); + } } diff --git a/tests/project.rs b/tests/project.rs index 99dca4ec..5dda8442 100644 --- a/tests/project.rs +++ b/tests/project.rs @@ -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"); + 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![ + 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::>(); + assert!(compiler_errors.is_empty(), "{:?}", compiler_errors); +} + #[test] fn can_compile_std_json_input() { let tmp = TempProject::dapptools_init().unwrap();