playdate_build/assets/
resolver.rs

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 crate_root = crate_root.to_string_lossy();
24	// #[cfg(windows)]
25	// let crate_root = unixish_path_pattern(crate_root.as_ref());
26	// let crate_root = Path::new(crate_root.as_ref());
27
28	let glob = Glob::new(expr.as_ref()).map_err(|err| {
29		           // According wax's issue https://github.com/olson-sean-k/wax/issues/34
30		           // we doesn't support Windows absolute paths and hope to partially relative paths.
31		           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		                  // modify target path:
52		                  let new = if target.is_absolute() && target.starts_with(crate_root) {
53			                  // make it relative to crate_root:
54			                  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			                  // as-is
73			                  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
90// TODO: Tests for `sanitize_path_pattern`
91/// Adapt path to wax walker, so kind of "patternize" or "unixish".
92///
93/// On Windows makes given absolute path to look like POSIX or UNC:
94/// `C:/foo/bar/**` or `//./C:/foo/bar/**`.
95///
96/// In details:
97/// - replace all `\` with `/`
98/// - if pattern starts with `<driveletter>:`, escape it as `<driveletter>\`:
99///
100/// On unix does nothing.
101pub fn sanitize_path_pattern(path: &str) -> Cow<'_, str> {
102	// TODO: Before patternize use normalize/canonicalize, crates: dunce, normpath, or path-slash
103	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	/// Do not uses cache.
133	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	/// Uses cache if it has been set.
138	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		// Possible recursion for case "${VAR}" where $VAR="${VAR}"
143		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				// use cache if it is:
163				if let Some(cache) = cache {
164					replaced = replaced.replace(
165					                            full,
166					                            // get from cache or resolve+insert and then use it:
167					                            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	/// Do not uses cache.
184	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	/// Uses cache if it has been set.
203	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		/// The path to the file to include.
219		source: PathBuf,
220
221		/// Matched part of path.
222		/// Related path that should be preserved in the output.
223		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	// TODO: tests for `Match::set_target`
274	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	// TODO: source: Span<..>
329	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	// TODO: tests for `Expr::set`
381	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}