resources.rs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  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 => {
  229. self.glob_iter = None;
  230. return Some(Err(crate::Error::GlobPathNotFound(pattern.clone())));
  231. }
  232. }
  233. }
  234. self.current_path = Some(normalize(Path::new(pattern)));
  235. self.next_current_path()
  236. }
  237. }
  238. impl<'a> Iterator for ResourcePaths<'a> {
  239. type Item = crate::Result<PathBuf>;
  240. fn next(&mut self) -> Option<crate::Result<PathBuf>> {
  241. self.iter.next().map(|r| r.map(|res| res.path))
  242. }
  243. }
  244. impl<'a> Iterator for ResourcePathsIter<'a> {
  245. type Item = crate::Result<Resource>;
  246. fn next(&mut self) -> Option<crate::Result<Resource>> {
  247. if self.current_path.is_some() {
  248. return self.next_current_path();
  249. }
  250. if self.walk_iter.is_some() {
  251. match self.next_walk_iter() {
  252. Some(r) => return Some(r),
  253. None => self.walk_iter = None,
  254. }
  255. }
  256. if self.glob_iter.is_some() {
  257. match self.next_glob_iter() {
  258. Some(r) => return Some(r),
  259. None => self.glob_iter = None,
  260. }
  261. }
  262. self.next_pattern()
  263. }
  264. }
  265. #[cfg(test)]
  266. mod tests {
  267. use super::*;
  268. use std::fs;
  269. use std::path::Path;
  270. impl PartialEq for Resource {
  271. fn eq(&self, other: &Self) -> bool {
  272. self.path == other.path && self.target == other.target
  273. }
  274. }
  275. fn expected_resources(resources: &[(&str, &str)]) -> Vec<Resource> {
  276. resources
  277. .iter()
  278. .map(|(path, target)| Resource {
  279. path: Path::new(path).components().collect(),
  280. target: Path::new(target).components().collect(),
  281. })
  282. .collect()
  283. }
  284. fn setup_test_dirs() {
  285. let mut random = [0; 1];
  286. getrandom::getrandom(&mut random).unwrap();
  287. let temp = std::env::temp_dir();
  288. let temp = temp.join(format!("tauri_resource_paths_iter_test_{}", random[0]));
  289. let _ = fs::remove_dir_all(&temp);
  290. fs::create_dir_all(&temp).unwrap();
  291. std::env::set_current_dir(&temp).unwrap();
  292. let paths = [
  293. Path::new("src-tauri/tauri.conf.json"),
  294. Path::new("src-tauri/some-other-json.json"),
  295. Path::new("src-tauri/Cargo.toml"),
  296. Path::new("src-tauri/Tauri.toml"),
  297. Path::new("src-tauri/build.rs"),
  298. Path::new("src/assets/javascript.svg"),
  299. Path::new("src/assets/tauri.svg"),
  300. Path::new("src/assets/rust.svg"),
  301. Path::new("src/assets/lang/en.json"),
  302. Path::new("src/assets/lang/ar.json"),
  303. Path::new("src/sounds/lang/es.wav"),
  304. Path::new("src/sounds/lang/fr.wav"),
  305. Path::new("src/textures/ground/earth.tex"),
  306. Path::new("src/textures/ground/sand.tex"),
  307. Path::new("src/textures/water.tex"),
  308. Path::new("src/textures/fire.tex"),
  309. Path::new("src/tiles/sky/grey.tile"),
  310. Path::new("src/tiles/sky/yellow.tile"),
  311. Path::new("src/tiles/grass.tile"),
  312. Path::new("src/tiles/stones.tile"),
  313. Path::new("src/index.html"),
  314. Path::new("src/style.css"),
  315. Path::new("src/script.js"),
  316. Path::new("src/dir/another-dir/file1.txt"),
  317. Path::new("src/dir/another-dir2/file2.txt"),
  318. ];
  319. for path in paths {
  320. fs::create_dir_all(path.parent().unwrap()).unwrap();
  321. fs::write(path, "").unwrap();
  322. }
  323. }
  324. #[test]
  325. #[serial_test::serial(resources)]
  326. fn resource_paths_iter_slice_allow_walk() {
  327. setup_test_dirs();
  328. let dir = std::env::current_dir().unwrap().join("src-tauri");
  329. let _ = std::env::set_current_dir(dir);
  330. let resources = ResourcePaths::new(
  331. &[
  332. "../src/script.js".into(),
  333. "../src/assets".into(),
  334. "../src/index.html".into(),
  335. "../src/sounds".into(),
  336. "*.toml".into(),
  337. "*.conf.json".into(),
  338. ],
  339. true,
  340. )
  341. .iter()
  342. .flatten()
  343. .collect::<Vec<_>>();
  344. let expected = expected_resources(&[
  345. ("../src/script.js", "_up_/src/script.js"),
  346. (
  347. "../src/assets/javascript.svg",
  348. "_up_/src/assets/javascript.svg",
  349. ),
  350. ("../src/assets/tauri.svg", "_up_/src/assets/tauri.svg"),
  351. ("../src/assets/rust.svg", "_up_/src/assets/rust.svg"),
  352. ("../src/assets/lang/en.json", "_up_/src/assets/lang/en.json"),
  353. ("../src/assets/lang/ar.json", "_up_/src/assets/lang/ar.json"),
  354. ("../src/index.html", "_up_/src/index.html"),
  355. ("../src/sounds/lang/es.wav", "_up_/src/sounds/lang/es.wav"),
  356. ("../src/sounds/lang/fr.wav", "_up_/src/sounds/lang/fr.wav"),
  357. ("Cargo.toml", "Cargo.toml"),
  358. ("Tauri.toml", "Tauri.toml"),
  359. ("tauri.conf.json", "tauri.conf.json"),
  360. ]);
  361. assert_eq!(resources.len(), expected.len());
  362. for resource in expected {
  363. if !resources.contains(&resource) {
  364. panic!("{resource:?} was expected but not found in {resources:?}");
  365. }
  366. }
  367. }
  368. #[test]
  369. #[serial_test::serial(resources)]
  370. fn resource_paths_iter_slice_no_walk() {
  371. setup_test_dirs();
  372. let dir = std::env::current_dir().unwrap().join("src-tauri");
  373. let _ = std::env::set_current_dir(dir);
  374. let resources = ResourcePaths::new(
  375. &[
  376. "../src/script.js".into(),
  377. "../src/assets".into(),
  378. "../src/index.html".into(),
  379. "../src/sounds".into(),
  380. "*.toml".into(),
  381. "*.conf.json".into(),
  382. ],
  383. false,
  384. )
  385. .iter()
  386. .flatten()
  387. .collect::<Vec<_>>();
  388. let expected = expected_resources(&[
  389. ("../src/script.js", "_up_/src/script.js"),
  390. ("../src/index.html", "_up_/src/index.html"),
  391. ("Cargo.toml", "Cargo.toml"),
  392. ("Tauri.toml", "Tauri.toml"),
  393. ("tauri.conf.json", "tauri.conf.json"),
  394. ]);
  395. assert_eq!(resources.len(), expected.len());
  396. for resource in expected {
  397. if !resources.contains(&resource) {
  398. panic!("{resource:?} was expected but not found in {resources:?}");
  399. }
  400. }
  401. }
  402. #[test]
  403. #[serial_test::serial(resources)]
  404. fn resource_paths_iter_map_allow_walk() {
  405. setup_test_dirs();
  406. let dir = std::env::current_dir().unwrap().join("src-tauri");
  407. let _ = std::env::set_current_dir(dir);
  408. let resources = ResourcePaths::from_map(
  409. &std::collections::HashMap::from_iter([
  410. ("../src/script.js".into(), "main.js".into()),
  411. ("../src/assets".into(), "".into()),
  412. ("../src/index.html".into(), "frontend/index.html".into()),
  413. ("../src/sounds".into(), "voices".into()),
  414. ("../src/textures/*".into(), "textures".into()),
  415. ("../src/tiles/**/*".into(), "tiles".into()),
  416. ("*.toml".into(), "".into()),
  417. ("*.conf.json".into(), "json".into()),
  418. ("../non-existent-file".into(), "asd".into()), // invalid case
  419. ("../non/*".into(), "asd".into()), // invalid case
  420. ]),
  421. true,
  422. )
  423. .iter()
  424. .flatten()
  425. .collect::<Vec<_>>();
  426. let expected = expected_resources(&[
  427. ("../src/script.js", "main.js"),
  428. ("../src/assets/javascript.svg", "javascript.svg"),
  429. ("../src/assets/tauri.svg", "tauri.svg"),
  430. ("../src/assets/rust.svg", "rust.svg"),
  431. ("../src/assets/lang/en.json", "lang/en.json"),
  432. ("../src/assets/lang/ar.json", "lang/ar.json"),
  433. ("../src/index.html", "frontend/index.html"),
  434. ("../src/sounds/lang/es.wav", "voices/lang/es.wav"),
  435. ("../src/sounds/lang/fr.wav", "voices/lang/fr.wav"),
  436. ("../src/textures/water.tex", "textures/water.tex"),
  437. ("../src/textures/fire.tex", "textures/fire.tex"),
  438. ("../src/tiles/grass.tile", "tiles/grass.tile"),
  439. ("../src/tiles/stones.tile", "tiles/stones.tile"),
  440. ("../src/tiles/sky/grey.tile", "tiles/grey.tile"),
  441. ("../src/tiles/sky/yellow.tile", "tiles/yellow.tile"),
  442. ("Cargo.toml", "Cargo.toml"),
  443. ("Tauri.toml", "Tauri.toml"),
  444. ("tauri.conf.json", "json/tauri.conf.json"),
  445. ]);
  446. assert_eq!(resources.len(), expected.len());
  447. for resource in expected {
  448. if !resources.contains(&resource) {
  449. panic!("{resource:?} was expected but not found in {resources:?}");
  450. }
  451. }
  452. }
  453. #[test]
  454. #[serial_test::serial(resources)]
  455. fn resource_paths_iter_map_no_walk() {
  456. setup_test_dirs();
  457. let dir = std::env::current_dir().unwrap().join("src-tauri");
  458. let _ = std::env::set_current_dir(dir);
  459. let resources = ResourcePaths::from_map(
  460. &std::collections::HashMap::from_iter([
  461. ("../src/script.js".into(), "main.js".into()),
  462. ("../src/assets".into(), "".into()),
  463. ("../src/index.html".into(), "frontend/index.html".into()),
  464. ("../src/sounds".into(), "voices".into()),
  465. ("*.toml".into(), "".into()),
  466. ("*.conf.json".into(), "json".into()),
  467. ]),
  468. false,
  469. )
  470. .iter()
  471. .flatten()
  472. .collect::<Vec<_>>();
  473. let expected = expected_resources(&[
  474. ("../src/script.js", "main.js"),
  475. ("../src/index.html", "frontend/index.html"),
  476. ("Cargo.toml", "Cargo.toml"),
  477. ("Tauri.toml", "Tauri.toml"),
  478. ("tauri.conf.json", "json/tauri.conf.json"),
  479. ]);
  480. assert_eq!(resources.len(), expected.len());
  481. for resource in expected {
  482. if !resources.contains(&resource) {
  483. panic!("{resource:?} was expected but not found in {resources:?}");
  484. }
  485. }
  486. }
  487. #[test]
  488. #[serial_test::serial(resources)]
  489. fn resource_paths_errors() {
  490. setup_test_dirs();
  491. let dir = std::env::current_dir().unwrap().join("src-tauri");
  492. let _ = std::env::set_current_dir(dir);
  493. let resources = ResourcePaths::from_map(
  494. &std::collections::HashMap::from_iter([
  495. ("../non-existent-file".into(), "file".into()),
  496. ("../non-existent-dir".into(), "dir".into()),
  497. // exists but not allowed to walk
  498. ("../src".into(), "dir2".into()),
  499. // doesn't exist but it is a glob and will return an error
  500. ("../non-existent-glob-dir/*".into(), "glob".into()),
  501. // exists but only contains directories and will not produce any values
  502. ("../src/dir/*".into(), "dir3".into()),
  503. ]),
  504. false,
  505. )
  506. .iter()
  507. .collect::<Vec<_>>();
  508. assert_eq!(resources.len(), 4);
  509. assert!(resources.iter().all(|r| r.is_err()));
  510. // hashmap order is not guaranteed so we check the error variant exists and how many
  511. assert!(resources
  512. .iter()
  513. .any(|r| matches!(r, Err(crate::Error::ResourcePathNotFound(_)))));
  514. assert_eq!(
  515. resources
  516. .iter()
  517. .filter(|r| matches!(r, Err(crate::Error::ResourcePathNotFound(_))))
  518. .count(),
  519. 2
  520. );
  521. assert!(resources
  522. .iter()
  523. .any(|r| matches!(r, Err(crate::Error::NotAllowedToWalkDir(_)))));
  524. assert_eq!(
  525. resources
  526. .iter()
  527. .filter(|r| matches!(r, Err(crate::Error::NotAllowedToWalkDir(_))))
  528. .count(),
  529. 1
  530. );
  531. assert!(resources
  532. .iter()
  533. .any(|r| matches!(r, Err(crate::Error::GlobPathNotFound(_)))));
  534. assert_eq!(
  535. resources
  536. .iter()
  537. .filter(|r| matches!(r, Err(crate::Error::GlobPathNotFound(_))))
  538. .count(),
  539. 1
  540. );
  541. }
  542. }