diff --git a/.changes/resources-map-becoming-dirs.md b/.changes/resources-map-becoming-dirs.md new file mode 100644 index 000000000000..4e064ecb581e --- /dev/null +++ b/.changes/resources-map-becoming-dirs.md @@ -0,0 +1,5 @@ +--- +"tauri-utils": "patch:bug" +--- + +Fix `ResourcePaths` iterator returning an unexpected result for mapped resources, for example `"../resources/user.json": "resources/user.json"` generates this resource `resources/user.json/user.json` where it should generate just `resources/user.json`. diff --git a/Cargo.lock b/Cargo.lock index 6d7f1ad1df98..db82e8c60ed3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1005,6 +1005,21 @@ dependencies = [ "new_debug_unreachable", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -1012,6 +1027,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1066,6 +1082,7 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -3174,6 +3191,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scc" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3281c67bce3cc354216537112a1571d2c28b9e7d744a07ef79b43fad64386c" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.23" @@ -3220,6 +3246,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sdd" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c386cfeafd20018fe07344e72dc4787f3432911e6c35d399457d86d2f146c4" + [[package]] name = "security-framework" version = "2.11.0" @@ -3388,6 +3420,31 @@ dependencies = [ "syn 2.0.74", ] +[[package]] +name = "serial_test" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.74", +] + [[package]] name = "serialize-to-javascript" version = "0.1.1" @@ -3955,6 +4012,7 @@ dependencies = [ "serde-untagged", "serde_json", "serde_with", + "serial_test", "serialize-to-javascript", "swift-rs", "thiserror", diff --git a/core/tauri-utils/Cargo.toml b/core/tauri-utils/Cargo.toml index 18ea644968af..9da3f89411bb 100644 --- a/core/tauri-utils/Cargo.toml +++ b/core/tauri-utils/Cargo.toml @@ -47,6 +47,10 @@ serde-untagged = "0.1" [target."cfg(target_os = \"macos\")".dependencies] swift-rs = { version = "1.0.6", optional = true, features = [ "build" ] } +[dev-dependencies] +getrandom = { version = "0.2", features = [ "std" ] } +serial_test = "3.1" + [features] build = [ "proc-macro2", diff --git a/core/tauri-utils/src/lib.rs b/core/tauri-utils/src/lib.rs index 08434064be31..c9842dad3b42 100644 --- a/core/tauri-utils/src/lib.rs +++ b/core/tauri-utils/src/lib.rs @@ -372,6 +372,10 @@ pub enum Error { #[cfg(feature = "resources")] #[error("could not walk directory `{0}`, try changing `allow_walk` to true on the `ResourcePaths` constructor.")] NotAllowedToWalkDir(std::path::PathBuf), + /// Resourece path doesn't exist + #[cfg(feature = "resources")] + #[error("resource path `{0}` doesn't exist")] + ResourcePathNotFound(std::path::PathBuf), } /// Reconstructs a path from its components using the platform separator then converts it to String and removes UNC prefixes on Windows if it exists. diff --git a/core/tauri-utils/src/resources.rs b/core/tauri-utils/src/resources.rs index ccc786826923..fc0235ca4488 100644 --- a/core/tauri-utils/src/resources.rs +++ b/core/tauri-utils/src/resources.rs @@ -7,6 +7,8 @@ use std::{ path::{Component, Path, PathBuf}, }; +use walkdir::WalkDir; + /// Given a path (absolute or relative) to a resource file, returns the /// relative path from the bundle resources directory where that resource /// should be stored. @@ -24,6 +26,20 @@ pub fn resource_relpath(path: &Path) -> PathBuf { dest } +fn normalize(path: &Path) -> PathBuf { + let mut dest = PathBuf::new(); + for component in path.components() { + match component { + Component::Prefix(_) => {} + Component::RootDir => dest.push("/"), + Component::CurDir => {} + Component::ParentDir => dest.push(".."), + Component::Normal(string) => dest.push(string), + } + } + dest +} + /// Parses the external binaries to bundle, adding the target triple suffix to each of them. pub fn external_binaries(external_binaries: &[String], target_triple: &str) -> Vec { let mut paths = Vec::new(); @@ -42,6 +58,26 @@ pub fn external_binaries(external_binaries: &[String], target_triple: &str) -> V paths } +/// Information for a resource. +#[derive(Debug)] +pub struct Resource { + path: PathBuf, + target: PathBuf, +} + +impl Resource { + /// The path of the resource. + pub fn path(&self) -> &Path { + &self.path + } + + /// The target location of the resource. + pub fn target(&self) -> &Path { + &self.target + } +} + +#[derive(Debug)] enum PatternIter<'a> { Slice(std::slice::Iter<'a, String>), Map(std::collections::hash_map::Iter<'a, String, String>), @@ -58,12 +94,12 @@ impl<'a> ResourcePaths<'a> { ResourcePaths { iter: ResourcePathsIter { pattern_iter: PatternIter::Slice(patterns.iter()), - glob_iter: None, - walk_iter: None, allow_walk, + current_path: None, current_pattern: None, - current_pattern_is_valid: false, current_dest: None, + walk_iter: None, + glob_iter: None, }, } } @@ -73,12 +109,12 @@ impl<'a> ResourcePaths<'a> { ResourcePaths { iter: ResourcePathsIter { pattern_iter: PatternIter::Map(patterns.iter()), - glob_iter: None, - walk_iter: None, allow_walk, + current_path: None, current_pattern: None, - current_pattern_is_valid: false, current_dest: None, + walk_iter: None, + glob_iter: None, }, } } @@ -91,38 +127,143 @@ impl<'a> ResourcePaths<'a> { } /// Iterator of a [`ResourcePaths`]. +#[derive(Debug)] pub struct ResourcePathsIter<'a> { /// the patterns to iterate. pattern_iter: PatternIter<'a>, - /// the glob iterator if the path from the current iteration is a glob pattern. - glob_iter: Option, - /// the walkdir iterator if the path from the current iteration is a directory. - walk_iter: Option, /// whether the resource paths allows directories or not. allow_walk: bool, - /// the pattern of the current iteration. - current_pattern: Option<(String, PathBuf)>, - /// whether the current pattern is valid or not. - current_pattern_is_valid: bool, - /// Current destination path. Only set when the iterator comes from a Map. + + current_path: Option, + current_pattern: Option, current_dest: Option, -} -/// Information for a resource. -pub struct Resource { - path: PathBuf, - target: PathBuf, + walk_iter: Option, + glob_iter: Option, } -impl Resource { - /// The path of the resource. - pub fn path(&self) -> &Path { - &self.path +impl<'a> ResourcePathsIter<'a> { + fn next_glob_iter(&mut self) -> Option> { + let entry = self.glob_iter.as_mut().unwrap().next()?; + + let entry = match entry { + Ok(entry) => entry, + Err(err) => return Some(Err(err.into())), + }; + + self.current_path = Some(normalize(&entry)); + self.next_current_path() } - /// The target location of the resource. - pub fn target(&self) -> &Path { - &self.target + fn next_walk_iter(&mut self) -> Option> { + let entry = self.walk_iter.as_mut().unwrap().next()?; + + let entry = match entry { + Ok(entry) => entry, + Err(err) => return Some(Err(err.into())), + }; + + self.current_path = Some(normalize(entry.path())); + self.next_current_path() + } + + fn resource_from_path(&mut self, path: &Path) -> crate::Result { + if !path.exists() { + return Err(crate::Error::ResourcePathNotFound(path.to_path_buf())); + } + + Ok(Resource { + path: path.to_path_buf(), + target: self + .current_dest + .as_ref() + .map(|current_dest| { + // if processing a directory, preserve directory structure under current_dest + if self.walk_iter.is_some() { + let current_pattern = self.current_pattern.as_ref().unwrap(); + current_dest.join(path.strip_prefix(current_pattern).unwrap_or(path)) + } else if current_dest.components().count() == 0 { + // if current_dest is empty while processing a file pattern or glob + // we preserve the file name as it is + PathBuf::from(path.file_name().unwrap()) + } else if self.glob_iter.is_some() { + // if processing a glob and current_dest is not empty + // we put all globbed paths under current_dest + // preserving the file name as it is + current_dest.join(path.file_name().unwrap()) + } else { + current_dest.clone() + } + }) + .unwrap_or_else(|| resource_relpath(path)), + }) + } + + fn next_current_path(&mut self) -> Option> { + // should be safe to unwrap since every call to `self.next_current_path()` + // is preceeded with assignemt to `self.current_path` + let path = self.current_path.take().unwrap(); + + let is_dir = path.is_dir(); + + if is_dir { + if self.glob_iter.is_some() { + return self.next(); + } + + if !self.allow_walk { + return Some(Err(crate::Error::NotAllowedToWalkDir(path.to_path_buf()))); + } + + if self.walk_iter.is_none() { + self.walk_iter = Some(WalkDir::new(&path).into_iter()); + } + + match self.next_walk_iter() { + Some(resource) => Some(resource), + None => { + self.walk_iter = None; + self.next() + } + } + } else { + Some(self.resource_from_path(&path)) + } + } + + fn next_pattern(&mut self) -> Option> { + self.current_pattern = None; + self.current_dest = None; + self.current_path = None; + + let pattern = match &mut self.pattern_iter { + PatternIter::Slice(iter) => match iter.next() { + Some(pattern) => pattern, + None => return None, + }, + PatternIter::Map(iter) => match iter.next() { + Some((pattern, dest)) => { + self.current_pattern = Some(pattern.clone()); + self.current_dest = Some(resource_relpath(Path::new(dest))); + pattern + } + None => return None, + }, + }; + + if pattern.contains('*') { + self.glob_iter = match glob::glob(pattern) { + Ok(glob) => Some(glob), + Err(error) => return Some(Err(error.into())), + }; + match self.next_glob_iter() { + Some(r) => return Some(r), + None => self.glob_iter = None, + } + } + + self.current_path = Some(normalize(Path::new(pattern))); + self.next_current_path() } } @@ -134,116 +275,279 @@ impl<'a> Iterator for ResourcePaths<'a> { } } -fn normalize(path: &Path) -> PathBuf { - let mut dest = PathBuf::new(); - for component in path.components() { - match component { - Component::Prefix(_) => {} - Component::RootDir => dest.push("/"), - Component::CurDir => {} - Component::ParentDir => dest.push(".."), - Component::Normal(string) => dest.push(string), +impl<'a> Iterator for ResourcePathsIter<'a> { + type Item = crate::Result; + + fn next(&mut self) -> Option> { + if self.current_path.is_some() { + return self.next_current_path(); } + + if self.walk_iter.is_some() { + match self.next_walk_iter() { + Some(r) => return Some(r), + None => self.walk_iter = None, + } + } + + if self.glob_iter.is_some() { + match self.next_glob_iter() { + Some(r) => return Some(r), + None => self.glob_iter = None, + } + } + + self.next_pattern() } - dest } -impl<'a> Iterator for ResourcePathsIter<'a> { - type Item = crate::Result; +#[cfg(test)] +mod tests { - fn next(&mut self) -> Option> { - loop { - if let Some(ref mut walk_entries) = self.walk_iter { - if let Some(entry) = walk_entries.next() { - let entry = match entry { - Ok(entry) => entry, - Err(error) => return Some(Err(crate::Error::from(error))), - }; - let path = entry.path(); - if path.is_dir() { - continue; - } - self.current_pattern_is_valid = true; - return Some(Ok(Resource { - target: if let (Some(current_dest), Some(current_pattern)) = - (&self.current_dest, &self.current_pattern) - { - if current_pattern.0.contains('*') { - current_dest.join(path.file_name().unwrap()) - } else { - current_dest.join(path.strip_prefix(¤t_pattern.1).unwrap()) - } - } else { - resource_relpath(path) - }, - path: path.to_path_buf(), - })); - } + use super::*; + use std::fs; + use std::path::Path; + + impl PartialEq for Resource { + fn eq(&self, other: &Self) -> bool { + self.path == other.path && self.target == other.target + } + } + + fn expected_resources(resources: &[(&str, &str)]) -> Vec { + resources + .iter() + .map(|(path, target)| Resource { + path: Path::new(path).components().collect(), + target: Path::new(target).components().collect(), + }) + .collect() + } + + fn setup_test_dirs() { + let mut random = [0; 1]; + getrandom::getrandom(&mut random).unwrap(); + + let temp = std::env::temp_dir(); + let temp = temp.join(format!("tauri_resource_paths_iter_test_{}", random[0])); + + let _ = fs::remove_dir_all(&temp); + fs::create_dir_all(&temp).unwrap(); + + std::env::set_current_dir(&temp).unwrap(); + + let paths = [ + Path::new("src-tauri/tauri.conf.json"), + Path::new("src-tauri/some-other-json.json"), + Path::new("src-tauri/Cargo.toml"), + Path::new("src-tauri/Tauri.toml"), + Path::new("src-tauri/build.rs"), + Path::new("src/assets/javascript.svg"), + Path::new("src/assets/tauri.svg"), + Path::new("src/assets/rust.svg"), + Path::new("src/assets/lang/en.json"), + Path::new("src/assets/lang/ar.json"), + Path::new("src/sounds/lang/es.wav"), + Path::new("src/sounds/lang/fr.wav"), + Path::new("src/textures/ground/earth.tex"), + Path::new("src/textures/ground/sand.tex"), + Path::new("src/textures/water.tex"), + Path::new("src/textures/fire.tex"), + Path::new("src/tiles/sky/grey.tile"), + Path::new("src/tiles/sky/yellow.tile"), + Path::new("src/tiles/grass.tile"), + Path::new("src/tiles/stones.tile"), + Path::new("src/index.html"), + Path::new("src/style.css"), + Path::new("src/script.js"), + ]; + + for path in paths { + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path, "").unwrap(); + } + } + + #[test] + #[serial_test::serial] + fn resource_paths_iter_slice_allow_walk() { + setup_test_dirs(); + + let dir = std::env::current_dir().unwrap().join("src-tauri"); + let _ = std::env::set_current_dir(dir); + + let resources = ResourcePaths::new( + &[ + "../src/script.js".into(), + "../src/assets".into(), + "../src/index.html".into(), + "../src/sounds".into(), + "*.toml".into(), + "*.conf.json".into(), + ], + true, + ) + .iter() + .flatten() + .collect::>(); + + let expected = expected_resources(&[ + ("../src/script.js", "_up_/src/script.js"), + ( + "../src/assets/javascript.svg", + "_up_/src/assets/javascript.svg", + ), + ("../src/assets/tauri.svg", "_up_/src/assets/tauri.svg"), + ("../src/assets/rust.svg", "_up_/src/assets/rust.svg"), + ("../src/assets/lang/en.json", "_up_/src/assets/lang/en.json"), + ("../src/assets/lang/ar.json", "_up_/src/assets/lang/ar.json"), + ("../src/index.html", "_up_/src/index.html"), + ("../src/sounds/lang/es.wav", "_up_/src/sounds/lang/es.wav"), + ("../src/sounds/lang/fr.wav", "_up_/src/sounds/lang/fr.wav"), + ("Cargo.toml", "Cargo.toml"), + ("Tauri.toml", "Tauri.toml"), + ("tauri.conf.json", "tauri.conf.json"), + ]); + + assert_eq!(resources.len(), expected.len()); + for resource in expected { + if !resources.contains(&resource) { + panic!("{resource:?} was expected but not found in {resources:?}"); } - self.walk_iter = None; - if let Some(ref mut glob_paths) = self.glob_iter { - if let Some(glob_result) = glob_paths.next() { - let path = match glob_result { - Ok(path) => path, - Err(error) => return Some(Err(error.into())), - }; - if path.is_dir() { - if self.allow_walk { - let walk = walkdir::WalkDir::new(path); - self.walk_iter = Some(walk.into_iter()); - continue; - } else { - return Some(Err(crate::Error::NotAllowedToWalkDir(path))); - } - } - self.current_pattern_is_valid = true; - return Some(Ok(Resource { - target: if let Some(current_dest) = &self.current_dest { - current_dest.join(path.file_name().unwrap()) - } else { - resource_relpath(&path) - }, - path, - })); - } else if let Some(current_path) = &self.current_pattern { - if !self.current_pattern_is_valid { - self.glob_iter = None; - return Some(Err(crate::Error::GlobPathNotFound(current_path.0.clone()))); - } - } + } + } + + #[test] + #[serial_test::serial] + fn resource_paths_iter_slice_no_walk() { + setup_test_dirs(); + + let dir = std::env::current_dir().unwrap().join("src-tauri"); + let _ = std::env::set_current_dir(dir); + + let resources = ResourcePaths::new( + &[ + "../src/script.js".into(), + "../src/assets".into(), + "../src/index.html".into(), + "../src/sounds".into(), + "*.toml".into(), + "*.conf.json".into(), + ], + false, + ) + .iter() + .flatten() + .collect::>(); + + let expected = expected_resources(&[ + ("../src/script.js", "_up_/src/script.js"), + ("../src/index.html", "_up_/src/index.html"), + ("Cargo.toml", "Cargo.toml"), + ("Tauri.toml", "Tauri.toml"), + ("tauri.conf.json", "tauri.conf.json"), + ]); + + assert_eq!(resources.len(), expected.len()); + for resource in expected { + if !resources.contains(&resource) { + panic!("{resource:?} was expected but not found in {resources:?}"); } - self.glob_iter = None; - self.current_dest = None; - match &mut self.pattern_iter { - PatternIter::Slice(iter) => { - if let Some(pattern) = iter.next() { - self.current_pattern = Some((pattern.to_string(), normalize(Path::new(pattern)))); - self.current_pattern_is_valid = false; - let glob = match glob::glob(pattern) { - Ok(glob) => glob, - Err(error) => return Some(Err(error.into())), - }; - self.glob_iter = Some(glob); - continue; - } - } - PatternIter::Map(iter) => { - if let Some((pattern, dest)) = iter.next() { - self.current_pattern = Some((pattern.to_string(), normalize(Path::new(pattern)))); - self.current_pattern_is_valid = false; - let glob = match glob::glob(pattern) { - Ok(glob) => glob, - Err(error) => return Some(Err(error.into())), - }; - self - .current_dest - .replace(resource_relpath(&PathBuf::from(dest))); - self.glob_iter = Some(glob); - continue; - } - } + } + } + + #[test] + #[serial_test::serial] + fn resource_paths_iter_map_allow_walk() { + setup_test_dirs(); + + let dir = std::env::current_dir().unwrap().join("src-tauri"); + let _ = std::env::set_current_dir(dir); + + let resources = ResourcePaths::from_map( + &std::collections::HashMap::from_iter([ + ("../src/script.js".into(), "main.js".into()), + ("../src/assets".into(), "".into()), + ("../src/index.html".into(), "frontend/index.html".into()), + ("../src/sounds".into(), "voices".into()), + ("../src/textures/*".into(), "textures".into()), + ("../src/tiles/**/*".into(), "tiles".into()), + ("*.toml".into(), "".into()), + ("*.conf.json".into(), "json".into()), + ("../non-existent-file".into(), "asd".into()), // invalid case + ("../non/*".into(), "asd".into()), // invalid case + ]), + true, + ) + .iter() + .flatten() + .collect::>(); + + let expected = expected_resources(&[ + ("../src/script.js", "main.js"), + ("../src/assets/javascript.svg", "javascript.svg"), + ("../src/assets/tauri.svg", "tauri.svg"), + ("../src/assets/rust.svg", "rust.svg"), + ("../src/assets/lang/en.json", "lang/en.json"), + ("../src/assets/lang/ar.json", "lang/ar.json"), + ("../src/index.html", "frontend/index.html"), + ("../src/sounds/lang/es.wav", "voices/lang/es.wav"), + ("../src/sounds/lang/fr.wav", "voices/lang/fr.wav"), + ("../src/textures/water.tex", "textures/water.tex"), + ("../src/textures/fire.tex", "textures/fire.tex"), + ("../src/tiles/grass.tile", "tiles/grass.tile"), + ("../src/tiles/stones.tile", "tiles/stones.tile"), + ("../src/tiles/sky/grey.tile", "tiles/grey.tile"), + ("../src/tiles/sky/yellow.tile", "tiles/yellow.tile"), + ("Cargo.toml", "Cargo.toml"), + ("Tauri.toml", "Tauri.toml"), + ("tauri.conf.json", "json/tauri.conf.json"), + ]); + + assert_eq!(resources.len(), expected.len()); + for resource in expected { + if !resources.contains(&resource) { + panic!("{resource:?} was expected but not found in {resources:?}"); + } + } + } + + #[test] + #[serial_test::serial] + fn resource_paths_iter_map_no_walk() { + setup_test_dirs(); + + let dir = std::env::current_dir().unwrap().join("src-tauri"); + let _ = std::env::set_current_dir(dir); + + let resources = ResourcePaths::from_map( + &std::collections::HashMap::from_iter([ + ("../src/script.js".into(), "main.js".into()), + ("../src/assets".into(), "".into()), + ("../src/index.html".into(), "frontend/index.html".into()), + ("../src/sounds".into(), "voices".into()), + ("*.toml".into(), "".into()), + ("*.conf.json".into(), "json".into()), + ]), + false, + ) + .iter() + .flatten() + .collect::>(); + + let expected = expected_resources(&[ + ("../src/script.js", "main.js"), + ("../src/index.html", "frontend/index.html"), + ("Cargo.toml", "Cargo.toml"), + ("Tauri.toml", "Tauri.toml"), + ("tauri.conf.json", "json/tauri.conf.json"), + ]); + + assert_eq!(resources.len(), expected.len()); + for resource in expected { + if !resources.contains(&resource) { + panic!("{resource:?} was expected but not found in {resources:?}"); } - return None; } } } diff --git a/examples/resources/src-tauri/src/main.rs b/examples/resources/src-tauri/src/main.rs index b27b4cbb7d9f..4b5320f68c40 100644 --- a/examples/resources/src-tauri/src/main.rs +++ b/examples/resources/src-tauri/src/main.rs @@ -8,7 +8,7 @@ use std::{ io::{BufRead, BufReader}, process::{Command, Stdio}, }; -use tauri::Manager; +use tauri::{Emitter, Manager}; fn main() { tauri::Builder::default()