foundry_block_explorers/
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                let with_extension = sanitized_path.with_extension("sol");
27                if !self.entries.iter().any(|e| e.path == with_extension) {
28                    sanitized_path = with_extension;
29                }
30            }
31            let joined = dir.join(sanitized_path);
32            if let Some(parent) = joined.parent() {
33                create_dir_all(parent)?;
34                std::fs::write(joined, &entry.contents)?;
35            }
36        }
37        Ok(())
38    }
39}
40
41/// Remove any components in a smart contract source path that could cause a directory traversal.
42pub(crate) fn sanitize_path(path: impl AsRef<Path>) -> PathBuf {
43    let sanitized = path
44        .as_ref()
45        .components()
46        .filter(|x| x.as_os_str() != Component::ParentDir.as_os_str())
47        .collect::<PathBuf>();
48
49    // Force absolute paths to be relative
50    sanitized.strip_prefix("/").map(PathBuf::from).unwrap_or(sanitized)
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use std::fs::read_dir;
57
58    /// Ensure that the source tree is written correctly and .sol extension is added to a path with
59    /// no extension.
60    #[test]
61    fn test_source_tree_write() {
62        let tempdir = tempfile::tempdir().unwrap();
63        let st = SourceTree {
64            entries: vec![
65                SourceTreeEntry { path: PathBuf::from("a/a.sol"), contents: String::from("Test") },
66                SourceTreeEntry { path: PathBuf::from("b/b"), contents: String::from("Test 2") },
67            ],
68        };
69        st.write_to(tempdir.path()).unwrap();
70        let a_sol_path = PathBuf::new().join(&tempdir).join("a").join("a.sol");
71        let b_sol_path = PathBuf::new().join(&tempdir).join("b").join("b.sol");
72        assert!(a_sol_path.exists());
73        assert!(b_sol_path.exists());
74    }
75
76    /// Ensure that the .. are ignored when writing the source tree to disk because of
77    /// sanitization.
78    #[test]
79    fn test_malformed_source_tree_write() {
80        let tempdir = tempfile::tempdir().unwrap();
81        let st = SourceTree {
82            entries: vec![
83                SourceTreeEntry {
84                    path: PathBuf::from("../a/a.sol"),
85                    contents: String::from("Test"),
86                },
87                SourceTreeEntry {
88                    path: PathBuf::from("../b/../b.sol"),
89                    contents: String::from("Test 2"),
90                },
91                SourceTreeEntry {
92                    path: PathBuf::from("/c/c.sol"),
93                    contents: String::from("Test 3"),
94                },
95            ],
96        };
97        st.write_to(tempdir.path()).unwrap();
98        let written_paths = read_dir(tempdir.path()).unwrap();
99        let paths: Vec<PathBuf> =
100            written_paths.into_iter().filter_map(|x| x.ok()).map(|x| x.path()).collect();
101        assert_eq!(paths.len(), 3);
102        assert!(paths.contains(&tempdir.path().join("a")));
103        assert!(paths.contains(&tempdir.path().join("b")));
104        assert!(paths.contains(&tempdir.path().join("c")));
105    }
106}