Skip to main content

huff_neo_utils/file/
remapper.rs

1use itertools::Itertools;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs;
5use std::io::{BufReader, Read};
6use std::path::{Path, PathBuf};
7
8/// A wrapper for dealing with Remappings
9#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone)]
10pub struct Remapper {
11    /// The remappings
12    pub remappings: HashMap<String, String>,
13    /// The base directory
14    pub base_dir: String,
15}
16
17impl Remapper {
18    /// Extracts remappings from `foundry.toml` and `remappings.txt`, and infers one for each library
19    /// in `lib/` that neither defines, as forge does.
20    pub fn new(root: impl AsRef<str>) -> Self {
21        let mut inner = HashMap::<String, String>::new();
22
23        // Gracefully parse remappings from foundry.toml
24        Remapper::from_foundry(root.as_ref(), &mut inner);
25
26        // And from remappings.txt
27        Remapper::from_file(root.as_ref(), &mut inner);
28
29        // And infer the rest from the installed libraries
30        Remapper::from_libs(root.as_ref(), &mut inner);
31
32        // Return the constructed remappings
33        Self { remappings: inner, base_dir: root.as_ref().to_string() }
34    }
35
36    /// Helper to break apart a remapping gracefully
37    pub fn split(remapping: &str) -> Option<(String, String)> {
38        let mut split = remapping.splitn(2, '=');
39        match split.next() {
40            Some(from) => split.next().map(|to| (from.to_string(), to.to_string())),
41            None => None,
42        }
43    }
44
45    /// Parse foundry toml remappings
46    pub fn from_foundry(root: &str, inner: &mut HashMap<String, String>) {
47        // Look for a `foundry.toml` file in the current directory.
48        let path = Path::new(root).join("foundry.toml");
49
50        match fs::File::open(&path) {
51            Ok(f) => {
52                // Open the buffered reader and read foundry.toml
53                let mut data = String::new();
54                let mut br = BufReader::new(f);
55
56                // Gracefully read foundry.toml
57                if let Err(e) = br.read_to_string(&mut data) {
58                    tracing::warn!(target: "parser", "Failed to read \"foundry.toml\" file contents!\nError: {:?}", e);
59                    return;
60                }
61
62                // Parse the foundry.toml file as toml
63                let toml = match toml::from_str::<toml::Value>(&data) {
64                    Ok(t) => t,
65                    Err(e) => {
66                        tracing::warn!(target: "parser", "\"foundry.toml\" incorrectly formatted: {:?}", e);
67                        return;
68                    }
69                };
70
71                // Parse the toml as a map
72                let toml_map = toml.as_table().cloned().unwrap_or_else(toml::map::Map::new);
73
74                // Transform the mappings into profiles
75                let profiles = toml_map.iter().filter_map(|p| p.1.as_table()).collect::<Vec<&toml::map::Map<String, toml::Value>>>();
76                let unwrapped_profiles = profiles.iter().flat_map(|t| t.values().collect_vec()).collect::<Vec<&toml::Value>>();
77
78                // Extract the inner tables from each profile
79                let inner_tables =
80                    unwrapped_profiles.iter().filter_map(|t| t.as_table()).collect::<Vec<&toml::map::Map<String, toml::Value>>>();
81                let unwrapped_inner_tables = inner_tables
82                    .iter()
83                    .flat_map(|t| t.into_iter().filter(|m| m.0.eq("remappings")).map(|m| m.1).collect_vec())
84                    .collect::<Vec<&toml::Value>>();
85
86                // Extract mappings that are arrays
87                let arr_mappings = unwrapped_inner_tables.iter().filter_map(|t| t.as_array()).collect::<Vec<&Vec<toml::Value>>>();
88                let unwrapped_mappings = arr_mappings.iter().cloned().flatten().collect::<Vec<&toml::Value>>();
89
90                // Filter the remappings as strings
91                let remapping_strings = unwrapped_mappings.iter().filter_map(|t| t.as_str()).collect::<Vec<&str>>();
92
93                // For each remapping string, try to split it and insert it into the remappings
94                remapping_strings.iter().for_each(|remapping| {
95                    match Remapper::split(remapping) {
96                        Some((from, to)) => {
97                            inner.insert(from, to);
98                        }
99                        None => tracing::warn!(target: "parser", "Failed to split remapping using \"=\" at \"{}\" in \"{}\"!", remapping, path.to_string_lossy()),
100                    }
101                });
102            }
103            Err(e) => {
104                tracing::warn!(target: "parser", "Foundry.toml not found in specified \"{}\"", root);
105                tracing::warn!(target: "parser", "{:?}", e);
106            }
107        }
108    }
109
110    /// Infers a remapping for each library in `lib/` without one, as forge does: `name/` to the
111    /// library's `src/` folder when it has one, or to the library itself
112    pub fn from_libs(root: &str, inner: &mut HashMap<String, String>) {
113        let Ok(entries) = fs::read_dir(Path::new(root).join("lib")) else {
114            return;
115        };
116        for entry in entries.filter_map(Result::ok).filter(|entry| entry.path().is_dir()) {
117            let name = entry.file_name().to_string_lossy().into_owned();
118            if name.starts_with('.') || inner.contains_key(&name) || inner.contains_key(&format!("{name}/")) {
119                continue;
120            }
121            let to = if entry.path().join("src").is_dir() { format!("lib/{name}/src/") } else { format!("lib/{name}/") };
122            inner.insert(format!("{name}/"), to);
123        }
124    }
125
126    /// Get remappings from a remappings.txt file
127    pub fn from_file(root: &str, inner: &mut HashMap<String, String>) {
128        let mut remappings: HashMap<String, String> = HashMap::new();
129        let remappings_file = PathBuf::new().join(root).join("remappings.txt");
130        if remappings_file.is_file() {
131            let content = fs::read_to_string(remappings_file).map_err(|err| err.to_string()).unwrap();
132
133            let rem_lines = content.lines().collect::<Vec<&str>>();
134            let rem = rem_lines.iter().filter(|l| l != &&"").map(|l| l.split_once('=')).collect::<Vec<Option<(&str, &str)>>>();
135            rem.iter().for_each(|pair| {
136                if let Some((lib, path)) = pair {
137                    remappings.insert(lib.to_string(), path.to_string());
138                }
139            });
140
141            inner.extend(remappings);
142        }
143    }
144}
145
146impl Remapper {
147    /// Remaps the start of a path with the remapping of the longest matching prefix, as forge does
148    pub fn remap(&self, path: &str) -> Option<String> {
149        let (from, to) = self.remappings.iter().filter(|(from, _)| path.starts_with(from.as_str())).max_by_key(|(from, _)| from.len())?;
150        tracing::debug!(target: "parser", "found key {} and value {}", from, to);
151        Some(format!("{}{to}{}", self.base_dir, &path[from.len()..]))
152    }
153}