ethers_etherscan/
source_tree.rs

1use crate::Result;
2use std::{
3    fs::create_dir_all,
4    path::{Component, Path, PathBuf},
5};
6
7#[derive(Clone, Debug)]
8pub struct SourceTreeEntry {
9    pub path: PathBuf,
10    pub contents: String,
11}
12
13#[derive(Clone, Debug)]
14pub struct SourceTree {
15    pub entries: Vec<SourceTreeEntry>,
16}
17
18impl SourceTree {
19    /// Expand the source tree into the provided directory.  This method sanitizes paths to ensure
20    /// that no directory traversal happens.
21    pub fn write_to(&self, dir: &Path) -> Result<()> {
22        create_dir_all(dir)?;
23        for entry in &self.entries {
24            let mut sanitized_path = sanitize_path(&entry.path);
25            if sanitized_path.extension().is_none() {
26                sanitized_path.set_extension("sol");
27            }
28            let joined = dir.join(sanitized_path);
29            if let Some(parent) = joined.parent() {
30                create_dir_all(parent)?;
31                std::fs::write(joined, &entry.contents)?;
32            }
33        }
34        Ok(())
35    }
36}
37
38/// Remove any components in a smart contract source path that could cause a directory traversal.
39fn sanitize_path(path: &Path) -> PathBuf {
40    let sanitized = Path::new(path)
41        .components()
42        .filter(|x| x.as_os_str() != Component::ParentDir.as_os_str())
43        .collect::<PathBuf>();
44
45    // Force absolute paths to be relative
46    sanitized.strip_prefix("/").map(PathBuf::from).unwrap_or(sanitized)
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use std::fs::read_dir;
53
54    /// Ensure that the source tree is written correctly and .sol extension is added to a path with
55    /// no extension.
56    #[test]
57    fn test_source_tree_write() {
58        let tempdir = tempfile::tempdir().unwrap();
59        let st = SourceTree {
60            entries: vec![
61                SourceTreeEntry { path: PathBuf::from("a/a.sol"), contents: String::from("Test") },
62                SourceTreeEntry { path: PathBuf::from("b/b"), contents: String::from("Test 2") },
63            ],
64        };
65        st.write_to(tempdir.path()).unwrap();
66        let a_sol_path = PathBuf::new().join(&tempdir).join("a").join("a.sol");
67        let b_sol_path = PathBuf::new().join(&tempdir).join("b").join("b.sol");
68        assert!(a_sol_path.exists());
69        assert!(b_sol_path.exists());
70    }
71
72    /// Ensure that the .. are ignored when writing the source tree to disk because of
73    /// sanitization.
74    #[test]
75    fn test_malformed_source_tree_write() {
76        let tempdir = tempfile::tempdir().unwrap();
77        let st = SourceTree {
78            entries: vec![
79                SourceTreeEntry {
80                    path: PathBuf::from("../a/a.sol"),
81                    contents: String::from("Test"),
82                },
83                SourceTreeEntry {
84                    path: PathBuf::from("../b/../b.sol"),
85                    contents: String::from("Test 2"),
86                },
87                SourceTreeEntry {
88                    path: PathBuf::from("/c/c.sol"),
89                    contents: String::from("Test 3"),
90                },
91            ],
92        };
93        st.write_to(tempdir.path()).unwrap();
94        let written_paths = read_dir(tempdir.path()).unwrap();
95        let paths: Vec<PathBuf> =
96            written_paths.into_iter().filter_map(|x| x.ok()).map(|x| x.path()).collect();
97        assert_eq!(paths.len(), 3);
98        assert!(paths.contains(&tempdir.path().join("a")));
99        assert!(paths.contains(&tempdir.path().join("b")));
100        assert!(paths.contains(&tempdir.path().join("c")));
101    }
102}