1use std::cell::RefCell;
2use std::collections::BTreeMap;
3use std::hash::Hash;
4use std::borrow::Cow;
5use std::str;
6use std::path::{Path, PathBuf, MAIN_SEPARATOR};
7
8use regex::Regex;
9use wax::{Glob, LinkBehavior, WalkError, WalkEntry};
10
11use crate::config::Env;
12use super::log_err;
13use super::Error;
14
15
16pub fn resolve_includes<S: AsRef<str>, Excl: AsRef<str>>(expr: S,
17 crate_root: &Path,
18 exclude: &[Excl],
19 links: LinkBehavior)
20 -> Result<Vec<Match>, Error> {
21 let expr = sanitize_path_pattern(expr.as_ref());
22
23 let glob = Glob::new(expr.as_ref()).map_err(|err| {
29 if cfg!(windows) {
32 let expr = PathBuf::from(expr.as_ref());
33 if expr.is_absolute() || expr.has_root() {
34 let issue = "Wax issue https://github.com/olson-sean-k/wax/issues/34";
35 Error::Error(format!("{err}, Windows absolute paths are not supported, {issue}"))
36 } else {
37 Error::from(err)
38 }
39 } else {
40 Error::from(err)
41 }
42 })?;
43 let exclude = exclude.iter().map(AsRef::as_ref).chain(["**/.*/**"]);
44 let walker = glob.walk_with_behavior(crate_root, links)
45 .not(exclude)?
46 .map(|res| res.map(Match::from));
47
48 let files = walker.map(|res| {
49 let mut inc = res.map_err(log_err)?;
50 let target = inc.target();
51 let new = if target.is_absolute() && target.starts_with(crate_root) {
53 if !cfg!(windows) {
55 let len = crate_root.components().count();
56 Some(target.components().skip(len).collect())
57 } else {
58 let target = target.display().to_string();
59 target.strip_prefix(&crate_root.display().to_string())
60 .map(|s| {
61 let mut s = Cow::from(s);
62 while let Some(stripped) = s.strip_prefix([MAIN_SEPARATOR, '/', '\\']) {
63 s = stripped.to_owned().into()
64 }
65 s.into_owned()
66 })
67 .map(PathBuf::from)
68 }
69 } else if target.is_absolute() {
70 Some(PathBuf::from(target.file_name().expect("target filename")))
71 } else {
72 None
74 };
75 if let Some(new) = new {
76 inc.set_target(new)
77 }
78 Ok::<_, WalkError>(inc)
79 });
80
81 let mut resolved = Vec::new();
82 for file in files {
83 resolved.push(file?);
84 }
85
86 Ok(resolved)
87}
88
89
90pub fn sanitize_path_pattern(path: &str) -> Cow<'_, str> {
102 if cfg!(windows) {
104 path.replace('\\', "/")
105 .replace(':', "\\:")
106 .replace("//", "/")
107 .into()
108 } else {
109 path.into()
110 }
111}
112
113
114pub struct EnvResolver(Regex, Option<RefCell<BTreeMap<String, String>>>);
115impl EnvResolver {
116 pub fn new() -> Self { Self(Regex::new(r"(\$\{([^}]+)\})").unwrap(), None) }
117
118 pub fn with_cache() -> Self {
119 let mut this = Self::new();
120 this.1 = Some(RefCell::new(BTreeMap::new()));
121 this
122 }
123
124 pub fn cache(&self) -> Option<std::cell::Ref<BTreeMap<String, String>>> { self.1.as_ref().map(|v| v.borrow()) }
125 pub fn into_cache(self) -> Option<BTreeMap<String, String>> { self.1.map(|cell| cell.into_inner()) }
126}
127impl Default for EnvResolver {
128 fn default() -> Self { Self::new() }
129}
130
131impl EnvResolver {
132 pub fn matches<'t, 's: 't>(&'t self, s: &'s str) -> impl Iterator<Item = regex::Match<'s>> + 't {
134 self.0.captures_iter(s.as_ref()).flat_map(|caps| caps.get(2))
135 }
136
137 pub fn str<S: AsRef<str>>(&self, s: S, env: &Env) -> String {
139 let re = &self.0;
140 let cache = self.1.as_ref();
141
142 let mut anti_recursion_counter: u8 = 42;
144
145 let mut replaced = String::from(s.as_ref());
146
147 fn resolve<'a>(name: &'a str, env: &'a Env) -> Cow<'a, str> {
148 env.vars
149 .get(name)
150 .map(Cow::from)
151 .or_else(|| std::env::var(name).map_err(log_err).ok().map(Cow::from))
152 .unwrap_or_else(|| name.into())
153 }
154
155 while re.is_match(replaced.as_str()) && anti_recursion_counter > 0 {
156 anti_recursion_counter -= 1;
157
158 if let Some(captures) = re.captures(replaced.as_str()) {
159 let full = &captures[0];
160 let name = &captures[2];
161
162 if let Some(cache) = cache {
164 replaced = replaced.replace(
165 full,
166 cache.borrow_mut()
168 .entry(name.to_owned())
169 .or_insert_with(|| resolve(name, env).into_owned())
170 .as_str(),
171 );
172 } else {
173 let s = resolve(name, env);
174 replaced = replaced.replace(full, &s);
175 }
176 } else {
177 break;
178 }
179 }
180 replaced
181 }
182
183 pub fn str_only<'c, S: AsRef<str>>(&self, s: S) -> Cow<'c, str> {
185 let re = &self.0;
186
187 let mut replaced = String::from(s.as_ref());
188 while re.is_match(replaced.as_str()) {
189 if let Some(captures) = re.captures(replaced.as_str()) {
190 let full = &captures[0];
191 let name = &captures[2];
192
193 let var = std::env::var(name).map_err(log_err)
194 .map(Cow::from)
195 .unwrap_or_else(|_| name.into());
196 replaced = replaced.replace(full, &var);
197 }
198 }
199 replaced.into()
200 }
201
202 pub fn expr<'e, Ex: AsMut<Expr<'e>>>(&self, mut expr: Ex, env: &Env) -> Ex {
204 let editable = expr.as_mut();
205 let replaced = self.str(editable.actual(), env);
206 if replaced != editable.actual() {
207 editable.set(replaced);
208 }
209 expr
210 }
211}
212
213
214#[derive(Debug)]
215pub enum Match {
216 Match(wax::WalkEntry<'static>),
217 Pair {
218 source: PathBuf,
220
221 target: PathBuf,
224 },
225}
226
227#[cfg(feature = "serde")]
228impl serde::Serialize for Match {
229 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
230 where S: serde::Serializer {
231 use serde::ser::SerializeStructVariant;
232 use self::Match::*;
233
234 let tag = match *self {
235 Pair { .. } => "Pair",
236 Match(..) => "Match",
237 };
238 let mut s = serializer.serialize_struct_variant("Match", 0, tag, 1)?;
239 s.serialize_field("source", &self.source())?;
240 s.serialize_field("target", &self.target())?;
241 s.end()
242 }
243}
244
245
246impl Eq for Match {}
247
248impl Match {
249 pub fn source(&self) -> Cow<Path> {
250 match self {
251 Match::Match(source) => Cow::Borrowed(source.path()),
252 Match::Pair { source, .. } => Cow::Borrowed(source.as_path()),
253 }
254 }
255 pub fn target(&self) -> Cow<Path> {
256 match self {
257 Match::Match(source) => Cow::Borrowed(Path::new(source.matched().complete())),
258 Match::Pair { target, .. } => Cow::Borrowed(target.as_path()),
259 }
260 }
261
262 pub fn into_parts(self) -> (PathBuf, PathBuf) {
263 match self {
264 Match::Match(source) => {
265 let target = source.matched().complete().into();
266 let source = source.into_path();
267 (source, target)
268 },
269 Match::Pair { source, target } => (source, target),
270 }
271 }
272
273 fn set_target<P: Into<PathBuf>>(&mut self, path: P) {
275 let path = path.into();
276 debug!("match: update target: {:?} <- {:?}", self.target(), path);
277 match self {
278 Match::Match(entry) => {
279 let mut new = Self::Pair { source: entry.path().into(),
280 target: path };
281 std::mem::swap(self, &mut new);
282 },
283 Match::Pair { target, .. } => {
284 let _ = std::mem::replace(target, path);
285 },
286 }
287 }
288}
289
290impl From<WalkEntry<'_>> for Match {
291 fn from(entry: WalkEntry) -> Self { Match::Match(entry.into_owned()) }
292}
293
294impl Match {
295 pub fn new<S: Into<PathBuf>, T: Into<PathBuf>>(source: S, target: T) -> Self {
296 Match::Pair { source: source.into(),
297 target: target.into() }
298 }
299}
300
301impl PartialEq for Match {
302 fn eq(&self, other: &Self) -> bool {
303 match (self, other) {
304 (Self::Match(l), Self::Match(r)) => {
305 l.path() == r.path() && l.matched().complete() == r.matched().complete()
306 },
307 (
308 Self::Pair { source: ls,
309 target: lt, },
310 Self::Pair { source: rs,
311 target: rt, },
312 ) => ls == rs && lt == rt,
313 _ => false,
314 }
315 }
316}
317
318impl Hash for Match {
319 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
320 core::mem::discriminant(self).hash(state);
321 self.source().hash(state);
322 }
323}
324
325
326#[derive(Debug, PartialEq, Eq, Hash, Clone)]
327pub enum Expr<'s> {
328 Original(String),
330 Modified { original: String, actual: Cow<'s, str> },
331}
332
333
334impl<T: ToString> From<T> for Expr<'_> {
335 fn from(source: T) -> Self { Self::Original(source.to_string()) }
336}
337
338impl<'e> AsMut<Expr<'e>> for Expr<'e> {
339 fn as_mut(&mut self) -> &mut Expr<'e> { self }
340}
341
342impl Expr<'_> {
343 pub fn as_str(&self) -> &str { self.actual() }
344}
345
346impl AsRef<str> for Expr<'_> {
347 fn as_ref(&self) -> &str { self.actual() }
348}
349
350impl From<&Expr<'_>> for PathBuf {
351 fn from(expr: &Expr<'_>) -> Self { expr.actual().into() }
352}
353
354impl From<Expr<'_>> for PathBuf {
355 fn from(expr: Expr<'_>) -> Self {
356 let actual: PathBuf = match expr {
357 Expr::Original(original) => original.into(),
358 Expr::Modified { actual, .. } => actual.into_owned().into(),
359 };
360 actual
361 }
362}
363
364impl Expr<'_> {
365 pub fn original(&self) -> &str {
366 match self {
367 Expr::Original(ref s) => s,
368 Expr::Modified { ref original, .. } => original,
369 }
370 }
371 pub fn actual(&self) -> &str {
372 match self {
373 Expr::Original(ref s) => s,
374 Expr::Modified { ref actual, .. } => actual,
375 }
376 }
377}
378
379impl<'e> Expr<'e> {
380 fn set<'s, S: Into<Cow<'s, str>>>(&mut self, actual: S)
382 where 's: 'e {
383 let original = match self {
384 Expr::Original(original) => original,
385 Expr::Modified { original, .. } => original,
386 };
387
388 let new = Self::Modified { original: std::mem::replace(original, String::with_capacity(0)),
389 actual: actual.into() };
390
391 let _ = std::mem::replace(self, new);
392 }
393}
394
395
396#[cfg(feature = "serde")]
397impl<'e> serde::Serialize for Expr<'e> {
398 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
399 where S: serde::Serializer {
400 use serde::ser::SerializeStructVariant;
401 use self::Expr::*;
402
403 match &self {
404 Original(original) => {
405 let mut s = serializer.serialize_struct_variant("Expr", 0, "Original", 1)?;
406 s.serialize_field("original", original)?;
407 s.end()
408 },
409 Modified { original, actual } => {
410 let mut s = serializer.serialize_struct_variant("Expr", 1, "Modified", 1)?;
411 s.serialize_field("original", original)?;
412 s.serialize_field("actual", actual)?;
413 s.end()
414 },
415 }
416 }
417}
418
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424
425 const LINKS: LinkBehavior = LinkBehavior::ReadTarget;
426
427 fn crate_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) }
428
429
430 #[test]
431 fn resolve_includes_one_exact() {
432 for file in ["Cargo.toml", "src/lib.rs"] {
433 let resolved = resolve_includes::<_, &str>(file, &crate_root(), &[], LINKS).unwrap();
434 assert_eq!(1, resolved.len());
435
436 let matched = resolved.first().unwrap();
437
438 assert_eq!(Path::new(file), matched.target());
439 assert_eq!(
440 Path::new(file).canonicalize().unwrap(),
441 matched.source().canonicalize().unwrap()
442 );
443 }
444 }
445
446 #[test]
447 fn resolve_includes_one_glob() {
448 for (file, expected) in [("Cargo.tom*", "Cargo.toml"), ("**/lib.rs", "src/lib.rs")] {
449 let resolved = resolve_includes::<_, &str>(file, &crate_root(), &[], LINKS).unwrap();
450 assert_eq!(1, resolved.len());
451
452 let matched = resolved.first().unwrap();
453
454 assert_eq!(Path::new(expected), matched.target());
455 assert_eq!(
456 Path::new(expected).canonicalize().unwrap(),
457 matched.source().canonicalize().unwrap()
458 );
459 }
460 }
461
462 #[test]
463 fn resolve_includes_many_glob() {
464 for (file, expected) in [
465 ("Cargo.*", &["Cargo.toml"][..]),
466 ("**/*.rs", &["src/lib.rs", "src/assets/mod.rs"][..]),
467 ] {
468 let resolved = resolve_includes::<_, &str>(file, &crate_root(), &[], LINKS).unwrap();
469 assert!(!resolved.is_empty());
470
471
472 let mut expected_passed = 0;
473
474 for expected in expected {
475 expected_passed += 1;
476 let expected = PathBuf::from(expected);
477 let matched = resolved.iter()
478 .find(|matched| matched.target() == expected)
479 .unwrap();
480
481 assert_eq!(expected.as_path(), matched.target());
482 assert_eq!(
483 expected.canonicalize().unwrap(),
484 matched.source().canonicalize().unwrap()
485 );
486 }
487
488 assert_eq!(expected.len(), expected_passed);
489 }
490 }
491
492 #[test]
493 fn resolve_includes_many_glob_exclude() {
494 let exclude = ["**/lib.*"];
495 for (file, expected) in [("Cargo.*", &["Cargo.toml"]), ("**/*.rs", &["src/assets/mod.rs"])] {
496 let resolved = resolve_includes::<_, &str>(file, &crate_root(), &exclude, LINKS).unwrap();
497 assert!(!resolved.is_empty());
498
499
500 let mut expected_passed = 0;
501
502 for expected in expected {
503 let matched = resolved.iter()
504 .find(|matched| matched.target() == Path::new(expected))
505 .unwrap();
506
507 assert_eq!(Path::new(expected), matched.target());
508 assert_eq!(
509 Path::new(expected).canonicalize().unwrap(),
510 matched.source().canonicalize().unwrap()
511 );
512 expected_passed += 1;
513 }
514
515 assert_eq!(expected.len(), expected_passed);
516 }
517 }
518
519 #[test]
520 #[cfg_attr(windows, should_panic)]
521 fn resolve_includes_glob_abs_to_local() {
522 let (file, expected) = (env!("CARGO_MANIFEST_DIR").to_owned() + "/Cargo.*", &["Cargo.toml"]);
523
524 let resolved = resolve_includes::<_, &str>(file, &crate_root(), &[], LINKS).unwrap();
525 assert_eq!(expected.len(), resolved.len());
526
527 let mut expected_passed = 0;
528
529 for expected in expected {
530 expected_passed += 1;
531 let matched = resolved.iter()
532 .find(|matched| matched.target() == Path::new(expected))
533 .unwrap();
534
535 assert_eq!(Path::new(expected), matched.target());
536 assert_eq!(
537 Path::new(expected).canonicalize().unwrap(),
538 matched.source().canonicalize().unwrap()
539 );
540 }
541
542 assert_eq!(expected.len(), expected_passed);
543 }
544
545
546 #[test]
547 fn resolver_expr() {
548 let resolver = EnvResolver::new();
549
550 let env = {
551 let mut env = Env::try_default().unwrap();
552 env.vars.insert("FOO".into(), "foo".into());
553 env.vars.insert("BAR".into(), "bar".into());
554 env
555 };
556
557 let exprs = [
558 ("${FOO}/file.txt", "foo/file.txt"),
559 ("${BAR}/file.txt", "bar/file.txt"),
560 ];
561
562 for (src, expected) in exprs {
563 let mut expr = Expr::from(src);
564 resolver.expr(&mut expr, &env);
565
566 assert_eq!(expected, expr.actual());
567 assert_eq!(expected, expr.as_str());
568 assert_eq!(src, expr.original());
569 }
570 }
571
572 #[test]
573 fn resolver_missed() {
574 let resolver = EnvResolver::new();
575 let env = Env::try_default().unwrap();
576 let mut expr = Expr::from("${MISSED}/file.txt");
577 resolver.expr(&mut expr, &env);
578
579 assert_eq!("MISSED/file.txt", expr.actual());
580 assert_eq!("MISSED/file.txt", expr.as_str());
581 assert_eq!("${MISSED}/file.txt", expr.original());
582 }
583
584 #[test]
585 fn resolver_recursion() {
586 let resolver = EnvResolver::new();
587 let mut env = Env::try_default().unwrap();
588 env.vars.insert("VAR".into(), "${VAR}".into());
589 let expr = Expr::from("${VAR}/file.txt");
590 resolver.expr(expr, &env);
591 }
592}