foundry_compilers_artifacts_solc/remappings/
mod.rs

1use serde::{Deserialize, Serialize};
2use std::{
3    fmt,
4    path::{Path, PathBuf},
5    str::FromStr,
6};
7
8#[cfg(feature = "walkdir")]
9mod find;
10
11/// The solidity compiler can only reference files that exist locally on your computer.
12/// So importing directly from GitHub (as an example) is not possible.
13///
14/// Let's imagine you want to use OpenZeppelin's amazing library of smart contracts,
15/// `@openzeppelin/contracts-ethereum-package`:
16///
17/// ```ignore
18/// pragma solidity 0.5.11;
19///
20/// import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
21///
22/// contract MyContract {
23///     using SafeMath for uint256;
24///     ...
25/// }
26/// ```
27///
28/// When using `solc`, you have to specify the following:
29///
30/// - A `prefix`: the path that's used in your smart contract, i.e.
31///   `@openzeppelin/contracts-ethereum-package`
32/// - A `target`: the absolute path of the downloaded contracts on your computer
33///
34/// The format looks like this: `solc prefix=target ./MyContract.sol`
35///
36/// For example:
37///
38/// ```text
39/// solc --bin \
40///     @openzeppelin/contracts-ethereum-package=/Your/Absolute/Path/To/@openzeppelin/contracts-ethereum-package \
41///     ./MyContract.sol
42/// ```
43///
44/// You can also specify a `context` which limits the scope of the remapping to a subset of your
45/// project. This allows you to apply the remapping only to imports located in a specific library or
46/// a specific file. Without a context a remapping is applied to every matching import in all files.
47///
48/// The format is: `solc context:prefix=target ./MyContract.sol`
49///
50/// [Source](https://ethereum.stackexchange.com/questions/74448/what-are-remappings-and-how-do-they-work-in-solidity)
51#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
52pub struct Remapping {
53    pub context: Option<String>,
54    pub name: String,
55    pub path: String,
56}
57
58impl Remapping {
59    /// Convenience function for [`RelativeRemapping::new`]
60    pub fn into_relative(self, root: &Path) -> RelativeRemapping {
61        RelativeRemapping::new(self, root)
62    }
63
64    /// Removes the `base` path from the remapping
65    pub fn strip_prefix(&mut self, base: &Path) -> &mut Self {
66        if let Ok(stripped) = Path::new(&self.path).strip_prefix(base) {
67            self.path = stripped.display().to_string();
68        }
69        self
70    }
71}
72
73#[derive(Debug, PartialEq, Eq, PartialOrd, thiserror::Error)]
74pub enum RemappingError {
75    #[error("invalid remapping format, found `{0}`, expected `<key>=<value>`")]
76    InvalidRemapping(String),
77    #[error("remapping key can't be empty, found `{0}`, expected `<key>=<value>`")]
78    EmptyRemappingKey(String),
79    #[error("remapping value must be a path, found `{0}`, expected `<key>=<value>`")]
80    EmptyRemappingValue(String),
81}
82
83impl FromStr for Remapping {
84    type Err = RemappingError;
85
86    fn from_str(remapping: &str) -> Result<Self, Self::Err> {
87        let (name, path) = remapping
88            .split_once('=')
89            .ok_or_else(|| RemappingError::InvalidRemapping(remapping.to_string()))?;
90        let (context, name) = name
91            .split_once(':')
92            .map_or((None, name), |(context, name)| (Some(context.to_string()), name));
93        if name.trim().is_empty() {
94            return Err(RemappingError::EmptyRemappingKey(remapping.to_string()));
95        }
96        if path.trim().is_empty() {
97            return Err(RemappingError::EmptyRemappingValue(remapping.to_string()));
98        }
99        // if the remapping just starts with : (no context name), treat it as global
100        let context = context.filter(|c| !c.trim().is_empty());
101        Ok(Self { context, name: name.to_string(), path: path.to_string() })
102    }
103}
104
105impl Serialize for Remapping {
106    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
107    where
108        S: serde::ser::Serializer,
109    {
110        serializer.serialize_str(&self.to_string())
111    }
112}
113
114impl<'de> Deserialize<'de> for Remapping {
115    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
116    where
117        D: serde::de::Deserializer<'de>,
118    {
119        let remapping = String::deserialize(deserializer)?;
120        Self::from_str(&remapping).map_err(serde::de::Error::custom)
121    }
122}
123
124// Remappings are printed as `prefix=target`
125impl fmt::Display for Remapping {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        let mut s = String::new();
128        if let Some(context) = self.context.as_ref() {
129            #[cfg(target_os = "windows")]
130            {
131                // ensure we have `/` slashes on windows
132                use path_slash::PathExt;
133                s.push_str(&std::path::Path::new(context).to_slash_lossy());
134            }
135            #[cfg(not(target_os = "windows"))]
136            {
137                s.push_str(context);
138            }
139            s.push(':');
140        }
141        let name =
142            if !self.name.ends_with('/') { format!("{}/", self.name) } else { self.name.clone() };
143        s.push_str(&{
144            #[cfg(target_os = "windows")]
145            {
146                // ensure we have `/` slashes on windows
147                use path_slash::PathExt;
148                format!("{}={}", name, std::path::Path::new(&self.path).to_slash_lossy())
149            }
150            #[cfg(not(target_os = "windows"))]
151            {
152                format!("{}={}", name, self.path)
153            }
154        });
155
156        if !s.ends_with('/') {
157            s.push('/');
158        }
159        f.write_str(&s)
160    }
161}
162
163impl Remapping {
164    /// Converts any `\\` separators in the `path` to `/`.
165    pub fn slash_path(&mut self) {
166        #[cfg(windows)]
167        {
168            use path_slash::PathExt;
169            self.path = Path::new(&self.path).to_slash_lossy().to_string();
170            if let Some(context) = self.context.as_mut() {
171                *context = Path::new(&context).to_slash_lossy().to_string();
172            }
173        }
174    }
175}
176
177/// A relative [`Remapping`] that's aware of the current location
178///
179/// See [`RelativeRemappingPathBuf`]
180#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
181pub struct RelativeRemapping {
182    pub context: Option<String>,
183    pub name: String,
184    pub path: RelativeRemappingPathBuf,
185}
186
187impl RelativeRemapping {
188    /// Creates a new `RelativeRemapping` starting prefixed with `root`
189    pub fn new(remapping: Remapping, root: &Path) -> Self {
190        Self {
191            context: remapping.context.map(|c| {
192                RelativeRemappingPathBuf::with_root(root, c).path.to_string_lossy().to_string()
193            }),
194            name: remapping.name,
195            path: RelativeRemappingPathBuf::with_root(root, remapping.path),
196        }
197    }
198
199    /// Converts this relative remapping into an absolute remapping
200    ///
201    /// This sets to root of the remapping to the given `root` path
202    pub fn to_remapping(mut self, root: PathBuf) -> Remapping {
203        self.path.parent = Some(root);
204        self.into()
205    }
206
207    /// Converts this relative remapping into [`Remapping`] without the root path
208    pub fn to_relative_remapping(mut self) -> Remapping {
209        self.path.parent.take();
210        self.into()
211    }
212}
213
214// Remappings are printed as `prefix=target`
215impl fmt::Display for RelativeRemapping {
216    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217        let mut s = String::new();
218        if let Some(context) = self.context.as_ref() {
219            #[cfg(target_os = "windows")]
220            {
221                // ensure we have `/` slashes on windows
222                use path_slash::PathExt;
223                s.push_str(&std::path::Path::new(context).to_slash_lossy());
224            }
225            #[cfg(not(target_os = "windows"))]
226            {
227                s.push_str(context);
228            }
229            s.push(':');
230        }
231        s.push_str(&{
232            #[cfg(target_os = "windows")]
233            {
234                // ensure we have `/` slashes on windows
235                use path_slash::PathExt;
236                format!("{}={}", self.name, self.path.original().to_slash_lossy())
237            }
238            #[cfg(not(target_os = "windows"))]
239            {
240                format!("{}={}", self.name, self.path.original().display())
241            }
242        });
243
244        if !s.ends_with('/') {
245            s.push('/');
246        }
247        f.write_str(&s)
248    }
249}
250
251impl From<RelativeRemapping> for Remapping {
252    fn from(r: RelativeRemapping) -> Self {
253        let RelativeRemapping { context, mut name, path } = r;
254        let mut path = path.relative().display().to_string();
255        if !path.ends_with('/') {
256            path.push('/');
257        }
258        if !name.ends_with('/') {
259            name.push('/');
260        }
261        Self { context, name, path }
262    }
263}
264
265impl From<Remapping> for RelativeRemapping {
266    fn from(r: Remapping) -> Self {
267        Self { context: r.context, name: r.name, path: r.path.into() }
268    }
269}
270
271/// The path part of the [`Remapping`] that knows the path of the file it was configured in, if any.
272///
273/// A [`Remapping`] is intended to be absolute, but paths in configuration files are often desired
274/// to be relative to the configuration file itself. For example, a path of
275/// `weird-erc20/=lib/weird-erc20/src/` configured in a file `/var/foundry.toml` might be desired to
276/// resolve as a `weird-erc20/=/var/lib/weird-erc20/src/` remapping.
277#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
278pub struct RelativeRemappingPathBuf {
279    pub parent: Option<PathBuf>,
280    pub path: PathBuf,
281}
282
283impl RelativeRemappingPathBuf {
284    /// Creates a new `RelativeRemappingPathBuf` that checks if the `path` is a child path of
285    /// `parent`.
286    pub fn with_root(
287        parent: impl AsRef<Path> + Into<PathBuf>,
288        path: impl AsRef<Path> + Into<PathBuf>,
289    ) -> Self {
290        if let Ok(path) = path.as_ref().strip_prefix(parent.as_ref()) {
291            Self { parent: Some(parent.into()), path: path.to_path_buf() }
292        } else if path.as_ref().has_root() {
293            Self { parent: None, path: path.into() }
294        } else {
295            Self { parent: Some(parent.into()), path: path.into() }
296        }
297    }
298
299    /// Returns the path as it was declared, without modification.
300    pub fn original(&self) -> &Path {
301        &self.path
302    }
303
304    /// Returns this path relative to the file it was declared in, if any.
305    /// Returns the original if this path was not declared in a file or if the
306    /// path has a root.
307    pub fn relative(&self) -> PathBuf {
308        if self.original().has_root() {
309            return self.original().into();
310        }
311        self.parent
312            .as_ref()
313            .map(|p| p.join(self.original()))
314            .unwrap_or_else(|| self.original().into())
315    }
316}
317
318impl<P: Into<PathBuf>> From<P> for RelativeRemappingPathBuf {
319    fn from(path: P) -> Self {
320        Self { parent: None, path: path.into() }
321    }
322}
323
324impl Serialize for RelativeRemapping {
325    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
326    where
327        S: serde::ser::Serializer,
328    {
329        serializer.serialize_str(&self.to_string())
330    }
331}
332
333impl<'de> Deserialize<'de> for RelativeRemapping {
334    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
335    where
336        D: serde::de::Deserializer<'de>,
337    {
338        let remapping = String::deserialize(deserializer)?;
339        let remapping = Remapping::from_str(&remapping).map_err(serde::de::Error::custom)?;
340        Ok(Self { context: remapping.context, name: remapping.name, path: remapping.path.into() })
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    pub use super::*;
347    pub use similar_asserts::assert_eq;
348
349    #[test]
350    fn relative_remapping() {
351        let remapping = "oz=a/b/c/d";
352        let remapping = Remapping::from_str(remapping).unwrap();
353
354        let relative = RelativeRemapping::new(remapping.clone(), Path::new("a/b/c"));
355        assert_eq!(relative.path.relative(), Path::new(&remapping.path));
356        assert_eq!(relative.path.original(), Path::new("d"));
357
358        let relative = RelativeRemapping::new(remapping.clone(), Path::new("x/y"));
359        assert_eq!(relative.path.relative(), Path::new("x/y/a/b/c/d"));
360        assert_eq!(relative.path.original(), Path::new(&remapping.path));
361
362        let remapping = "oz=/a/b/c/d";
363        let remapping = Remapping::from_str(remapping).unwrap();
364        let relative = RelativeRemapping::new(remapping.clone(), Path::new("a/b"));
365        assert_eq!(relative.path.relative(), Path::new(&remapping.path));
366        assert_eq!(relative.path.original(), Path::new(&remapping.path));
367        assert!(relative.path.parent.is_none());
368
369        let relative = RelativeRemapping::new(remapping, Path::new("/a/b"));
370        assert_eq!(relative.to_relative_remapping(), Remapping::from_str("oz/=c/d/").unwrap());
371    }
372
373    #[test]
374    fn remapping_errors() {
375        let remapping = "oz=../b/c/d";
376        let remapping = Remapping::from_str(remapping).unwrap();
377        assert_eq!(remapping.name, "oz".to_string());
378        assert_eq!(remapping.path, "../b/c/d".to_string());
379
380        let err = Remapping::from_str("").unwrap_err();
381        matches!(err, RemappingError::InvalidRemapping(_));
382
383        let err = Remapping::from_str("oz=").unwrap_err();
384        matches!(err, RemappingError::EmptyRemappingValue(_));
385    }
386
387    #[test]
388    fn can_resolve_contexts() {
389        let remapping = "context:oz=a/b/c/d";
390        let remapping = Remapping::from_str(remapping).unwrap();
391
392        assert_eq!(
393            remapping,
394            Remapping {
395                context: Some("context".to_string()),
396                name: "oz".to_string(),
397                path: "a/b/c/d".to_string(),
398            }
399        );
400        assert_eq!(remapping.to_string(), "context:oz/=a/b/c/d/".to_string());
401
402        let remapping = "context:foo=C:/bar/src/";
403        let remapping = Remapping::from_str(remapping).unwrap();
404
405        assert_eq!(
406            remapping,
407            Remapping {
408                context: Some("context".to_string()),
409                name: "foo".to_string(),
410                path: "C:/bar/src/".to_string()
411            }
412        );
413    }
414
415    #[test]
416    fn can_resolve_global_contexts() {
417        let remapping = ":oz=a/b/c/d/";
418        let remapping = Remapping::from_str(remapping).unwrap();
419
420        assert_eq!(
421            remapping,
422            Remapping { context: None, name: "oz".to_string(), path: "a/b/c/d/".to_string() }
423        );
424        assert_eq!(remapping.to_string(), "oz/=a/b/c/d/".to_string());
425    }
426}