resources.rs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use std::{
  5. collections::HashMap,
  6. path::{Component, Path, PathBuf},
  7. };
  8. use walkdir::WalkDir;
  9. /// Given a path (absolute or relative) to a resource file, returns the
  10. /// relative path from the bundle resources directory where that resource
  11. /// should be stored.
  12. pub fn resource_relpath(path: &Path) -> PathBuf {
  13. let mut dest = PathBuf::new();
  14. for component in path.components() {
  15. match component {
  16. Component::Prefix(_) => {}
  17. Component::RootDir => dest.push("_root_"),
  18. Component::CurDir => {}
  19. Component::ParentDir => dest.push("_up_"),
  20. Component::Normal(string) => dest.push(string),
  21. }
  22. }
  23. dest
  24. }
  25. fn normalize(path: &Path) -> PathBuf {
  26. let mut dest = PathBuf::new();
  27. for component in path.components() {
  28. match component {
  29. Component::Prefix(_) => {}
  30. Component::RootDir => dest.push("/"),
  31. Component::CurDir => {}
  32. Component::ParentDir => dest.push(".."),
  33. Component::Normal(string) => dest.push(string),
  34. }
  35. }
  36. dest
  37. }
  38. /// Parses the external binaries to bundle, adding the target triple suffix to each of them.
  39. pub fn external_binaries(external_binaries: &[String], target_triple: &str) -> Vec<String> {
  40. let mut paths = Vec::new();
  41. for curr_path in external_binaries {
  42. paths.push(format!(
  43. "{}-{}{}",
  44. curr_path,
  45. target_triple,
  46. if target_triple.contains("windows") {
  47. ".exe"
  48. } else {
  49. ""
  50. }
  51. ));
  52. }
  53. paths
  54. }
  55. /// Information for a resource.
  56. #[derive(Debug)]
  57. pub struct Resource {
  58. path: PathBuf,
  59. target: PathBuf,
  60. }
  61. impl Resource {
  62. /// The path of the resource.
  63. pub fn path(&self) -> &Path {
  64. &self.path
  65. }
  66. /// The target location of the resource.
  67. pub fn target(&self) -> &Path {
  68. &self.target
  69. }
  70. }
  71. #[derive(Debug)]
  72. enum PatternIter<'a> {
  73. Slice(std::slice::Iter<'a, String>),
  74. Map(std::collections::hash_map::Iter<'a, String, String>),
  75. }
  76. /// A helper to iterate through resources.
  77. pub struct ResourcePaths<'a> {
  78. iter: ResourcePathsIter<'a>,
  79. }
  80. impl<'a> ResourcePaths<'a> {
  81. /// Creates a new ResourcePaths from a slice of patterns to iterate
  82. pub fn new(patterns: &'a [String], allow_walk: bool) -> ResourcePaths<'a> {
  83. ResourcePaths {
  84. iter: ResourcePathsIter {
  85. pattern_iter: PatternIter::Slice(patterns.iter()),
  86. allow_walk,
  87. current_path: None,
  88. current_pattern: None,
  89. current_dest: None,
  90. walk_iter: None,
  91. glob_iter: None,
  92. },
  93. }
  94. }
  95. /// Creates a new ResourcePaths from a slice of patterns to iterate
  96. pub fn from_map(patterns: &'a HashMap<String, String>, allow_walk: bool) -> ResourcePaths<'a> {
  97. ResourcePaths {
  98. iter: ResourcePathsIter {
  99. pattern_iter: PatternIter::Map(patterns.iter()),
  100. allow_walk,
  101. current_path: None,
  102. current_pattern: None,
  103. current_dest: None,
  104. walk_iter: None,
  105. glob_iter: None,
  106. },
  107. }
  108. }
  109. /// Returns the resource iterator that yields the source and target paths.
  110. /// Needed when using [`Self::from_map`].
  111. pub fn iter(self) -> ResourcePathsIter<'a> {
  112. self.iter
  113. }
  114. }
  115. /// Iterator of a [`ResourcePaths`].
  116. #[derive(Debug)]
  117. pub struct ResourcePathsIter<'a> {
  118. /// the patterns to iterate.
  119. pattern_iter: PatternIter<'a>,
  120. /// whether the resource paths allows directories or not.
  121. allow_walk: bool,
  122. current_path: Option<PathBuf>,
  123. current_pattern: Option<String>,
  124. current_dest: Option<PathBuf>,
  125. walk_iter: Option<walkdir::IntoIter>,
  126. glob_iter: Option<glob::Paths>,
  127. }
  128. impl<'a> ResourcePathsIter<'a> {
  129. fn next_glob_iter(&mut self) -> Option<crate::Result<Resource>> {
  130. let entry = self.glob_iter.as_mut().unwrap().next()?;
  131. let entry = match entry {
  132. Ok(entry) => entry,
  133. Err(err) => return Some(Err(err.into())),
  134. };
  135. self.current_path = Some(normalize(&entry));
  136. self.next_current_path()
  137. }
  138. fn next_walk_iter(&mut self) -> Option<crate::Result<Resource>> {
  139. let entry = self.walk_iter.as_mut().unwrap().next()?;
  140. let entry = match entry {
  141. Ok(entry) => entry,
  142. Err(err) => return Some(Err(err.into())),
  143. };
  144. self.current_path = Some(normalize(entry.path()));
  145. self.next_current_path()
  146. }
  147. fn resource_from_path(&mut self, path: &Path) -> crate::Result<Resource> {
  148. if !path.exists() {
  149. return Err(crate::Error::ResourcePathNotFound(path.to_path_buf()));
  150. }
  151. Ok(Resource {
  152. path: path.to_path_buf(),
  153. target: self
  154. .current_dest
  155. .as_ref()
  156. .map(|current_dest| {
  157. // if processing a directory, preserve directory structure under current_dest
  158. if self.walk_iter.is_some() {
  159. let current_pattern = self.current_pattern.as_ref().unwrap();
  160. current_dest.join(path.strip_prefix(current_pattern).unwrap_or(path))
  161. } else if current_dest.components().count() == 0 {
  162. // if current_dest is empty while processing a file pattern or glob
  163. // we preserve the file name as it is
  164. PathBuf::from(path.file_name().unwrap())
  165. } else if self.glob_iter.is_some() {
  166. // if processing a glob and current_dest is not empty
  167. // we put all globbed paths under current_dest
  168. // preserving the file name as it is
  169. current_dest.join(path.file_name().unwrap())
  170. } else {
  171. current_dest.clone()
  172. }
  173. })
  174. .unwrap_or_else(|| resource_relpath(path)),
  175. })
  176. }
  177. fn next_current_path(&mut self) -> Option<crate::Result<Resource>> {
  178. // should be safe to unwrap since every call to `self.next_current_path()`
  179. // is preceeded with assignemt to `self.current_path`
  180. let path = self.current_path.take().unwrap();
  181. let is_dir = path.is_dir();
  182. if is_dir {
  183. if self.glob_iter.is_some() {
  184. return self.next();
  185. }
  186. if !self.allow_walk {
  187. return Some(Err(crate::Error::NotAllowedToWalkDir(path.to_path_buf())));
  188. }
  189. if self.walk_iter.is_none() {
  190. self.walk_iter = Some(WalkDir::new(&path).into_iter());
  191. }
  192. match self.next_walk_iter() {
  193. Some(resource) => Some(resource),
  194. None => {
  195. self.walk_iter = None;
  196. self.next()
  197. }
  198. }
  199. } else {
  200. Some(self.resource_from_path(&path))
  201. }
  202. }
  203. fn next_pattern(&mut self) -> Option<crate::Result<Resource>> {
  204. self.current_pattern = None;
  205. self.current_dest = None;
  206. self.current_path = None;
  207. let pattern = match &mut self.pattern_iter {
  208. PatternIter::Slice(iter) => match iter.next() {
  209. Some(pattern) => pattern,
  210. None => return None,
  211. },
  212. PatternIter::Map(iter) => match iter.next() {
  213. Some((pattern, dest)) => {
  214. self.current_pattern = Some(pattern.clone());
  215. self.current_dest = Some(resource_relpath(Path::new(dest)));
  216. pattern
  217. }
  218. None => return None,
  219. },
  220. };
  221. if pattern.contains('*') {
  222. self.glob_iter = match glob::glob(pattern) {
  223. Ok(glob) => Some(glob),
  224. Err(error) => return Some(Err(error.into())),
  225. };
  226. match self.next_glob_iter() {
  227. Some(r) => return Some(r),
  228. None => self.glob_iter = None,
  229. }
  230. }
  231. self.current_path = Some(normalize(Path::new(pattern)));
  232. self.next_current_path()
  233. }
  234. }
  235. impl<'a> Iterator for ResourcePaths<'a> {
  236. type Item = crate::Result<PathBuf>;
  237. fn next(&mut self) -> Option<crate::Result<PathBuf>> {
  238. self.iter.next().map(|r| r.map(|res| res.path))
  239. }
  240. }
  241. impl<'a> Iterator for ResourcePathsIter<'a> {
  242. type Item = crate::Result<Resource>;
  243. fn next(&mut self) -> Option<crate::Result<Resource>> {
  244. if self.current_path.is_some() {
  245. return self.next_current_path();
  246. }
  247. if self.walk_iter.is_some() {
  248. match self.next_walk_iter() {
  249. Some(r) => return Some(r),
  250. None => self.walk_iter = None,
  251. }
  252. }
  253. if self.glob_iter.is_some() {
  254. match self.next_glob_iter() {
  255. Some(r) => return Some(r),
  256. None => self.glob_iter = None,
  257. }
  258. }
  259. self.next_pattern()
  260. }
  261. }
  262. #[cfg(test)]
  263. mod tests {
  264. use super::*;
  265. use std::fs;
  266. use std::path::Path;
  267. impl PartialEq for Resource {
  268. fn eq(&self, other: &Self) -> bool {
  269. self.path == other.path && self.target == other.target
  270. }
  271. }
  272. fn expected_resources(resources: &[(&str, &str)]) -> Vec<Resource> {
  273. resources
  274. .iter()
  275. .map(|(path, target)| Resource {
  276. path: Path::new(path).components().collect(),
  277. target: Path::new(target).components().collect(),
  278. })
  279. .collect()
  280. }
  281. fn setup_test_dirs() {
  282. let mut random = [0; 1];
  283. getrandom::getrandom(&mut random).unwrap();
  284. let temp = std::env::temp_dir();
  285. let temp = temp.join(format!("tauri_resource_paths_iter_test_{}", random[0]));
  286. let _ = fs::remove_dir_all(&temp);
  287. fs::create_dir_all(&temp).unwrap();
  288. std::env::set_current_dir(&temp).unwrap();
  289. let paths = [
  290. Path::new("src-tauri/tauri.conf.json"),
  291. Path::new("src-tauri/some-other-json.json"),
  292. Path::new("src-tauri/Cargo.toml"),
  293. Path::new("src-tauri/Tauri.toml"),
  294. Path::new("src-tauri/build.rs"),
  295. Path::new("src/assets/javascript.svg"),
  296. Path::new("src/assets/tauri.svg"),
  297. Path::new("src/assets/rust.svg"),
  298. Path::new("src/assets/lang/en.json"),
  299. Path::new("src/assets/lang/ar.json"),
  300. Path::new("src/sounds/lang/es.wav"),
  301. Path::new("src/sounds/lang/fr.wav"),
  302. Path::new("src/textures/ground/earth.tex"),
  303. Path::new("src/textures/ground/sand.tex"),
  304. Path::new("src/textures/water.tex"),
  305. Path::new("src/textures/fire.tex"),
  306. Path::new("src/tiles/sky/grey.tile"),
  307. Path::new("src/tiles/sky/yellow.tile"),
  308. Path::new("src/tiles/grass.tile"),
  309. Path::new("src/tiles/stones.tile"),
  310. Path::new("src/index.html"),
  311. Path::new("src/style.css"),
  312. Path::new("src/script.js"),
  313. ];
  314. for path in paths {
  315. fs::create_dir_all(path.parent().unwrap()).unwrap();
  316. fs::write(path, "").unwrap();
  317. }
  318. }
  319. #[test]
  320. #[serial_test::serial]
  321. fn resource_paths_iter_slice_allow_walk() {
  322. setup_test_dirs();
  323. let dir = std::env::current_dir().unwrap().join("src-tauri");
  324. let _ = std::env::set_current_dir(dir);
  325. let resources = ResourcePaths::new(
  326. &[
  327. "../src/script.js".into(),
  328. "../src/assets".into(),
  329. "../src/index.html".into(),
  330. "../src/sounds".into(),
  331. "*.toml".into(),
  332. "*.conf.json".into(),
  333. ],
  334. true,
  335. )
  336. .iter()
  337. .flatten()
  338. .collect::<Vec<_>>();
  339. let expected = expected_resources(&[
  340. ("../src/script.js", "_up_/src/script.js"),
  341. (
  342. "../src/assets/javascript.svg",
  343. "_up_/src/assets/javascript.svg",
  344. ),
  345. ("../src/assets/tauri.svg", "_up_/src/assets/tauri.svg"),
  346. ("../src/assets/rust.svg", "_up_/src/assets/rust.svg"),
  347. ("../src/assets/lang/en.json", "_up_/src/assets/lang/en.json"),
  348. ("../src/assets/lang/ar.json", "_up_/src/assets/lang/ar.json"),
  349. ("../src/index.html", "_up_/src/index.html"),
  350. ("../src/sounds/lang/es.wav", "_up_/src/sounds/lang/es.wav"),
  351. ("../src/sounds/lang/fr.wav", "_up_/src/sounds/lang/fr.wav"),
  352. ("Cargo.toml", "Cargo.toml"),
  353. ("Tauri.toml", "Tauri.toml"),
  354. ("tauri.conf.json", "tauri.conf.json"),
  355. ]);
  356. assert_eq!(resources.len(), expected.len());
  357. for resource in expected {
  358. if !resources.contains(&resource) {
  359. panic!("{resource:?} was expected but not found in {resources:?}");
  360. }
  361. }
  362. }
  363. #[test]
  364. #[serial_test::serial]
  365. fn resource_paths_iter_slice_no_walk() {
  366. setup_test_dirs();
  367. let dir = std::env::current_dir().unwrap().join("src-tauri");
  368. let _ = std::env::set_current_dir(dir);
  369. let resources = ResourcePaths::new(
  370. &[
  371. "../src/script.js".into(),
  372. "../src/assets".into(),
  373. "../src/index.html".into(),
  374. "../src/sounds".into(),
  375. "*.toml".into(),
  376. "*.conf.json".into(),
  377. ],
  378. false,
  379. )
  380. .iter()
  381. .flatten()
  382. .collect::<Vec<_>>();
  383. let expected = expected_resources(&[
  384. ("../src/script.js", "_up_/src/script.js"),
  385. ("../src/index.html", "_up_/src/index.html"),
  386. ("Cargo.toml", "Cargo.toml"),
  387. ("Tauri.toml", "Tauri.toml"),
  388. ("tauri.conf.json", "tauri.conf.json"),
  389. ]);
  390. assert_eq!(resources.len(), expected.len());
  391. for resource in expected {
  392. if !resources.contains(&resource) {
  393. panic!("{resource:?} was expected but not found in {resources:?}");
  394. }
  395. }
  396. }
  397. #[test]
  398. #[serial_test::serial]
  399. fn resource_paths_iter_map_allow_walk() {
  400. setup_test_dirs();
  401. let dir = std::env::current_dir().unwrap().join("src-tauri");
  402. let _ = std::env::set_current_dir(dir);
  403. let resources = ResourcePaths::from_map(
  404. &std::collections::HashMap::from_iter([
  405. ("../src/script.js".into(), "main.js".into()),
  406. ("../src/assets".into(), "".into()),
  407. ("../src/index.html".into(), "frontend/index.html".into()),
  408. ("../src/sounds".into(), "voices".into()),
  409. ("../src/textures/*".into(), "textures".into()),
  410. ("../src/tiles/**/*".into(), "tiles".into()),
  411. ("*.toml".into(), "".into()),
  412. ("*.conf.json".into(), "json".into()),
  413. ("../non-existent-file".into(), "asd".into()), // invalid case
  414. ("../non/*".into(), "asd".into()), // invalid case
  415. ]),
  416. true,
  417. )
  418. .iter()
  419. .flatten()
  420. .collect::<Vec<_>>();
  421. let expected = expected_resources(&[
  422. ("../src/script.js", "main.js"),
  423. ("../src/assets/javascript.svg", "javascript.svg"),
  424. ("../src/assets/tauri.svg", "tauri.svg"),
  425. ("../src/assets/rust.svg", "rust.svg"),
  426. ("../src/assets/lang/en.json", "lang/en.json"),
  427. ("../src/assets/lang/ar.json", "lang/ar.json"),
  428. ("../src/index.html", "frontend/index.html"),
  429. ("../src/sounds/lang/es.wav", "voices/lang/es.wav"),
  430. ("../src/sounds/lang/fr.wav", "voices/lang/fr.wav"),
  431. ("../src/textures/water.tex", "textures/water.tex"),
  432. ("../src/textures/fire.tex", "textures/fire.tex"),
  433. ("../src/tiles/grass.tile", "tiles/grass.tile"),
  434. ("../src/tiles/stones.tile", "tiles/stones.tile"),
  435. ("../src/tiles/sky/grey.tile", "tiles/grey.tile"),
  436. ("../src/tiles/sky/yellow.tile", "tiles/yellow.tile"),
  437. ("Cargo.toml", "Cargo.toml"),
  438. ("Tauri.toml", "Tauri.toml"),
  439. ("tauri.conf.json", "json/tauri.conf.json"),
  440. ]);
  441. assert_eq!(resources.len(), expected.len());
  442. for resource in expected {
  443. if !resources.contains(&resource) {
  444. panic!("{resource:?} was expected but not found in {resources:?}");
  445. }
  446. }
  447. }
  448. #[test]
  449. #[serial_test::serial]
  450. fn resource_paths_iter_map_no_walk() {
  451. setup_test_dirs();
  452. let dir = std::env::current_dir().unwrap().join("src-tauri");
  453. let _ = std::env::set_current_dir(dir);
  454. let resources = ResourcePaths::from_map(
  455. &std::collections::HashMap::from_iter([
  456. ("../src/script.js".into(), "main.js".into()),
  457. ("../src/assets".into(), "".into()),
  458. ("../src/index.html".into(), "frontend/index.html".into()),
  459. ("../src/sounds".into(), "voices".into()),
  460. ("*.toml".into(), "".into()),
  461. ("*.conf.json".into(), "json".into()),
  462. ]),
  463. false,
  464. )
  465. .iter()
  466. .flatten()
  467. .collect::<Vec<_>>();
  468. let expected = expected_resources(&[
  469. ("../src/script.js", "main.js"),
  470. ("../src/index.html", "frontend/index.html"),
  471. ("Cargo.toml", "Cargo.toml"),
  472. ("Tauri.toml", "Tauri.toml"),
  473. ("tauri.conf.json", "json/tauri.conf.json"),
  474. ]);
  475. assert_eq!(resources.len(), expected.len());
  476. for resource in expected {
  477. if !resources.contains(&resource) {
  478. panic!("{resource:?} was expected but not found in {resources:?}");
  479. }
  480. }
  481. }
  482. }