foundry_compilers_artifacts_solc/remappings/
mod.rs1use serde::{Deserialize, Serialize};
2use std::{
3 fmt,
4 path::{Path, PathBuf},
5 str::FromStr,
6};
7
8#[cfg(feature = "walkdir")]
9mod find;
10
11#[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 pub fn into_relative(self, root: &Path) -> RelativeRemapping {
61 RelativeRemapping::new(self, root)
62 }
63
64 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 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
124impl 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 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 = if needs_trailing_slash(&self.name) {
142 format!("{}/", self.name)
143 } else {
144 self.name.clone()
145 };
146 s.push_str(&{
147 #[cfg(target_os = "windows")]
148 {
149 use path_slash::PathExt;
151 format!("{}={}", name, std::path::Path::new(&self.path).to_slash_lossy())
152 }
153 #[cfg(not(target_os = "windows"))]
154 {
155 format!("{}={}", name, self.path)
156 }
157 });
158
159 if needs_trailing_slash(&s) {
160 s.push('/');
161 }
162 f.write_str(&s)
163 }
164}
165
166impl Remapping {
167 pub fn slash_path(&mut self) {
169 #[cfg(windows)]
170 {
171 use path_slash::PathExt;
172 self.path = Path::new(&self.path).to_slash_lossy().to_string();
173 if let Some(context) = self.context.as_mut() {
174 *context = Path::new(&context).to_slash_lossy().to_string();
175 }
176 }
177 }
178}
179
180#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
184pub struct RelativeRemapping {
185 pub context: Option<String>,
186 pub name: String,
187 pub path: RelativeRemappingPathBuf,
188}
189
190impl RelativeRemapping {
191 pub fn new(remapping: Remapping, root: &Path) -> Self {
193 Self {
194 context: remapping.context.map(|c| {
195 RelativeRemappingPathBuf::with_root(root, c).path.to_string_lossy().to_string()
196 }),
197 name: remapping.name,
198 path: RelativeRemappingPathBuf::with_root(root, remapping.path),
199 }
200 }
201
202 pub fn to_remapping(mut self, root: PathBuf) -> Remapping {
206 self.path.parent = Some(root);
207 self.into()
208 }
209
210 pub fn to_relative_remapping(mut self) -> Remapping {
212 self.path.parent.take();
213 self.into()
214 }
215}
216
217impl fmt::Display for RelativeRemapping {
219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 let mut s = String::new();
221 if let Some(context) = self.context.as_ref() {
222 #[cfg(target_os = "windows")]
223 {
224 use path_slash::PathExt;
226 s.push_str(&std::path::Path::new(context).to_slash_lossy());
227 }
228 #[cfg(not(target_os = "windows"))]
229 {
230 s.push_str(context);
231 }
232 s.push(':');
233 }
234 s.push_str(&{
235 #[cfg(target_os = "windows")]
236 {
237 use path_slash::PathExt;
239 format!("{}={}", self.name, self.path.original().to_slash_lossy())
240 }
241 #[cfg(not(target_os = "windows"))]
242 {
243 format!("{}={}", self.name, self.path.original().display())
244 }
245 });
246
247 if needs_trailing_slash(&s) {
248 s.push('/');
249 }
250 f.write_str(&s)
251 }
252}
253
254impl From<RelativeRemapping> for Remapping {
255 fn from(r: RelativeRemapping) -> Self {
256 let RelativeRemapping { context, mut name, path } = r;
257 let mut path = path.relative().display().to_string();
258 if needs_trailing_slash(&path) {
259 path.push('/');
260 }
261 if needs_trailing_slash(&name) {
262 name.push('/');
263 }
264 Self { context, name, path }
265 }
266}
267
268impl From<Remapping> for RelativeRemapping {
269 fn from(r: Remapping) -> Self {
270 Self { context: r.context, name: r.name, path: r.path.into() }
271 }
272}
273
274#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
281pub struct RelativeRemappingPathBuf {
282 pub parent: Option<PathBuf>,
283 pub path: PathBuf,
284}
285
286impl RelativeRemappingPathBuf {
287 pub fn with_root(
290 parent: impl AsRef<Path> + Into<PathBuf>,
291 path: impl AsRef<Path> + Into<PathBuf>,
292 ) -> Self {
293 if let Ok(path) = path.as_ref().strip_prefix(parent.as_ref()) {
294 Self { parent: Some(parent.into()), path: path.to_path_buf() }
295 } else if path.as_ref().has_root() {
296 Self { parent: None, path: path.into() }
297 } else {
298 Self { parent: Some(parent.into()), path: path.into() }
299 }
300 }
301
302 pub fn original(&self) -> &Path {
304 &self.path
305 }
306
307 pub fn relative(&self) -> PathBuf {
311 if self.original().has_root() {
312 return self.original().into();
313 }
314 self.parent
315 .as_ref()
316 .map(|p| p.join(self.original()))
317 .unwrap_or_else(|| self.original().into())
318 }
319}
320
321impl<P: Into<PathBuf>> From<P> for RelativeRemappingPathBuf {
322 fn from(path: P) -> Self {
323 Self { parent: None, path: path.into() }
324 }
325}
326
327impl Serialize for RelativeRemapping {
328 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
329 where
330 S: serde::ser::Serializer,
331 {
332 serializer.serialize_str(&self.to_string())
333 }
334}
335
336impl<'de> Deserialize<'de> for RelativeRemapping {
337 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
338 where
339 D: serde::de::Deserializer<'de>,
340 {
341 let remapping = String::deserialize(deserializer)?;
342 let remapping = Remapping::from_str(&remapping).map_err(serde::de::Error::custom)?;
343 Ok(Self { context: remapping.context, name: remapping.name, path: remapping.path.into() })
344 }
345}
346
347fn needs_trailing_slash(name_or_path: &str) -> bool {
353 !name_or_path.ends_with('/') && !name_or_path.ends_with(".sol")
354}
355
356#[cfg(test)]
357mod tests {
358 pub use super::*;
359 pub use similar_asserts::assert_eq;
360
361 #[test]
362 fn relative_remapping() {
363 let remapping = "oz=a/b/c/d";
364 let remapping = Remapping::from_str(remapping).unwrap();
365
366 let relative = RelativeRemapping::new(remapping.clone(), Path::new("a/b/c"));
367 assert_eq!(relative.path.relative(), Path::new(&remapping.path));
368 assert_eq!(relative.path.original(), Path::new("d"));
369
370 let relative = RelativeRemapping::new(remapping.clone(), Path::new("x/y"));
371 assert_eq!(relative.path.relative(), Path::new("x/y/a/b/c/d"));
372 assert_eq!(relative.path.original(), Path::new(&remapping.path));
373
374 let remapping = "oz=/a/b/c/d";
375 let remapping = Remapping::from_str(remapping).unwrap();
376 let relative = RelativeRemapping::new(remapping.clone(), Path::new("a/b"));
377 assert_eq!(relative.path.relative(), Path::new(&remapping.path));
378 assert_eq!(relative.path.original(), Path::new(&remapping.path));
379 assert!(relative.path.parent.is_none());
380
381 let relative = RelativeRemapping::new(remapping, Path::new("/a/b"));
382 assert_eq!(relative.to_relative_remapping(), Remapping::from_str("oz/=c/d/").unwrap());
383 }
384
385 #[test]
386 fn remapping_errors() {
387 let remapping = "oz=../b/c/d";
388 let remapping = Remapping::from_str(remapping).unwrap();
389 assert_eq!(remapping.name, "oz".to_string());
390 assert_eq!(remapping.path, "../b/c/d".to_string());
391
392 let err = Remapping::from_str("").unwrap_err();
393 matches!(err, RemappingError::InvalidRemapping(_));
394
395 let err = Remapping::from_str("oz=").unwrap_err();
396 matches!(err, RemappingError::EmptyRemappingValue(_));
397 }
398
399 #[test]
400 fn can_resolve_contexts() {
401 let remapping = "context:oz=a/b/c/d";
402 let remapping = Remapping::from_str(remapping).unwrap();
403
404 assert_eq!(
405 remapping,
406 Remapping {
407 context: Some("context".to_string()),
408 name: "oz".to_string(),
409 path: "a/b/c/d".to_string(),
410 }
411 );
412 assert_eq!(remapping.to_string(), "context:oz/=a/b/c/d/".to_string());
413
414 let remapping = "context:foo=C:/bar/src/";
415 let remapping = Remapping::from_str(remapping).unwrap();
416
417 assert_eq!(
418 remapping,
419 Remapping {
420 context: Some("context".to_string()),
421 name: "foo".to_string(),
422 path: "C:/bar/src/".to_string()
423 }
424 );
425 }
426
427 #[test]
428 fn can_resolve_global_contexts() {
429 let remapping = ":oz=a/b/c/d/";
430 let remapping = Remapping::from_str(remapping).unwrap();
431
432 assert_eq!(
433 remapping,
434 Remapping { context: None, name: "oz".to_string(), path: "a/b/c/d/".to_string() }
435 );
436 assert_eq!(remapping.to_string(), "oz/=a/b/c/d/".to_string());
437 }
438
439 #[test]
441 fn can_preserve_single_sol_file_remapping() {
442 let remapping = "@my-lib/B.sol=lib/my-lib/B.sol";
443 let remapping = Remapping::from_str(remapping).unwrap();
444
445 assert_eq!(
446 remapping,
447 Remapping {
448 context: None,
449 name: "@my-lib/B.sol".to_string(),
450 path: "lib/my-lib/B.sol".to_string()
451 }
452 );
453 assert_eq!(remapping.to_string(), "@my-lib/B.sol=lib/my-lib/B.sol".to_string());
454 }
455}