use std::{
collections::HashMap,
path::{Component, Path, PathBuf},
};
use walkdir::WalkDir;
use crate::platform::Target as TargetPlatform;
pub fn resource_relpath(path: &Path) -> PathBuf {
let mut dest = PathBuf::new();
for component in path.components() {
match component {
Component::Prefix(_) => {}
Component::RootDir => dest.push("_root_"),
Component::CurDir => {}
Component::ParentDir => dest.push("_up_"),
Component::Normal(string) => dest.push(string),
}
}
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
}
pub fn external_binaries(
external_binaries: &[String],
target_triple: &str,
target_platform: &TargetPlatform,
) -> Vec<String> {
let mut paths = Vec::new();
for curr_path in external_binaries {
let extension = if matches!(target_platform, TargetPlatform::Windows) {
".exe"
} else {
""
};
paths.push(format!("{curr_path}-{target_triple}{extension}"));
}
paths
}
#[derive(Debug)]
pub struct Resource {
path: PathBuf,
target: PathBuf,
}
impl Resource {
pub fn path(&self) -> &Path {
&self.path
}
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>),
}
pub struct ResourcePaths<'a> {
iter: ResourcePathsIter<'a>,
}
impl<'a> ResourcePaths<'a> {
pub fn new(patterns: &'a [String], allow_walk: bool) -> ResourcePaths<'a> {
ResourcePaths {
iter: ResourcePathsIter {
pattern_iter: PatternIter::Slice(patterns.iter()),
allow_walk,
base_dir: None,
current_dest: None,
current_iter: None,
rerun_if_changed: Vec::new(),
},
}
}
pub fn from_map(patterns: &'a HashMap<String, String>, allow_walk: bool) -> ResourcePaths<'a> {
ResourcePaths {
iter: ResourcePathsIter {
pattern_iter: PatternIter::Map(patterns.iter()),
allow_walk,
base_dir: None,
current_dest: None,
current_iter: None,
rerun_if_changed: Vec::new(),
},
}
}
#[must_use]
pub fn with_base_dir(mut self, base_dir: impl Into<PathBuf>) -> Self {
self.iter.base_dir = Some(base_dir.into());
self
}
pub fn iter(self) -> ResourcePathsIter<'a> {
self.iter
}
}
#[derive(Debug)]
pub struct ResourcePathsIter<'a> {
pattern_iter: PatternIter<'a>,
allow_walk: bool,
base_dir: Option<PathBuf>,
current_dest: Option<PathBuf>,
current_iter: Option<ResourcePathsInnerIter>,
rerun_if_changed: Vec<PathBuf>,
}
#[derive(Debug)]
enum ResourcePathsInnerIter {
Walk {
iter: walkdir::IntoIter,
current_pattern: Option<PathBuf>,
},
Glob {
iter: glob::Paths,
},
}
impl Iterator for ResourcePathsInnerIter {
type Item = crate::Result<PathBuf>;
fn next(&mut self) -> Option<crate::Result<PathBuf>> {
match self {
ResourcePathsInnerIter::Walk { iter, .. } => Some(
iter
.next()?
.map(|entry| entry.into_path())
.map_err(Into::into),
),
ResourcePathsInnerIter::Glob { iter } => Some(iter.next()?.map_err(Into::into)),
}
}
}
impl ResourcePathsIter<'_> {
pub fn rerun_if_changed(&self) -> &[PathBuf] {
&self.rerun_if_changed
}
fn next_current_iter(&mut self) -> Option<crate::Result<Resource>> {
let current_iter = self.current_iter.as_mut().unwrap();
let entry = current_iter.next()?;
Some(match entry {
Ok(entry) => {
if entry.is_dir() {
self.next_current_iter()?
} else {
let entry = if self.base_dir.is_some() {
entry
} else {
normalize(&entry)
};
self.resource_from_path(entry)
}
}
Err(error) => Err(error),
})
}
fn resource_from_path(&self, path: PathBuf) -> crate::Result<Resource> {
if !path.exists() {
return Err(crate::Error::ResourcePathNotFound(path));
}
let relative_path: &Path = match &self.base_dir {
Some(base_dir) => path.strip_prefix(base_dir).unwrap_or(&path),
None => &path,
};
let target = if let Some(dest) = &self.current_dest {
match &self.current_iter {
Some(current_iter) => match current_iter {
ResourcePathsInnerIter::Walk {
current_pattern, ..
} => {
if let Some(pattern) = current_pattern {
dest.join(path.strip_prefix(pattern).unwrap_or(&path))
} else {
dest.join(relative_path)
}
}
ResourcePathsInnerIter::Glob { .. } => dest.join(path.file_name().unwrap()),
},
None => {
if dest.components().count() == 0 {
PathBuf::from(path.file_name().unwrap())
} else {
dest.clone()
}
}
}
} else {
resource_relpath(relative_path)
};
Ok(Resource { target, path })
}
fn next_pattern(&mut self) -> Option<crate::Result<Resource>> {
self.current_dest = None;
self.current_iter = None;
let pattern = match &mut self.pattern_iter {
PatternIter::Slice(iter) => iter.next()?,
PatternIter::Map(iter) => {
let (pattern, dest) = iter.next()?;
self.current_dest = Some(resource_relpath(Path::new(dest)));
pattern
}
};
if pattern.contains('*') {
let glob_pattern = match &self.base_dir {
Some(base_dir) => Path::new(&glob::Pattern::escape(&base_dir.to_string_lossy()))
.join(pattern)
.to_string_lossy()
.into_owned(),
None => pattern.clone(),
};
let mut base = PathBuf::new();
for component in Path::new(&glob_pattern).components() {
if component.as_os_str().to_string_lossy().contains('*') {
break;
}
base.push(component);
}
if base.as_os_str().is_empty() {
base.push(".");
}
self.rerun_if_changed.push(base);
self.current_iter = match glob::glob(&glob_pattern) {
Ok(glob) => Some(ResourcePathsInnerIter::Glob { iter: glob }),
Err(error) => return Some(Err(error.into())),
};
match self.next_current_iter() {
Some(r) => Some(r),
None => {
self.current_iter = None;
Some(Err(crate::Error::GlobPathNotFound(pattern.clone())))
}
}
} else {
let path = normalize(Path::new(pattern));
let path = match &self.base_dir {
Some(base_dir) => base_dir.join(path),
None => path,
};
self.rerun_if_changed.push(path.clone());
if path.is_dir() {
if !self.allow_walk {
return Some(Err(crate::Error::NotAllowedToWalkDir(path)));
}
self.current_iter = Some(ResourcePathsInnerIter::Walk {
iter: WalkDir::new(&path).into_iter(),
current_pattern: if matches!(self.pattern_iter, PatternIter::Map(_)) {
Some(path)
} else {
None
},
});
self.next_current_iter().or_else(|| self.next_pattern())
} else {
Some(self.resource_from_path(path))
}
}
}
}
impl Iterator for ResourcePaths<'_> {
type Item = crate::Result<PathBuf>;
fn next(&mut self) -> Option<crate::Result<PathBuf>> {
self.iter.next().map(|r| r.map(|res| res.path))
}
}
impl Iterator for ResourcePathsIter<'_> {
type Item = crate::Result<Resource>;
fn next(&mut self) -> Option<crate::Result<Resource>> {
if self.current_iter.is_some() {
match self.next_current_iter() {
Some(r) => return Some(r),
None => self.current_iter = None,
}
}
self.next_pattern()
}
}
#[cfg(test)]
mod tests {
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<Resource> {
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::fill(&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 = [
"src-tauri/tauri.conf.json",
"src-tauri/some-other-json.json",
"src-tauri/Cargo.toml",
"src-tauri/Tauri.toml",
"src-tauri/build.rs",
"src-tauri/some-folder/some-file.txt",
"src/assets/javascript.svg",
"src/assets/tauri.svg",
"src/assets/rust.svg",
"src/assets/lang/en.json",
"src/assets/lang/ar.json",
"src/sounds/lang/es.wav",
"src/sounds/lang/fr.wav",
"src/textures/ground/earth.tex",
"src/textures/ground/sand.tex",
"src/textures/water.tex",
"src/textures/fire.tex",
"src/tiles/sky/grey.tile",
"src/tiles/sky/yellow.tile",
"src/tiles/grass.tile",
"src/tiles/stones.tile",
"src/index.html",
"src/style.css",
"src/script.js",
"src/dir/another-dir/file1.txt",
"src/dir/another-dir2/file2.txt",
];
for path in paths {
let path = Path::new(path);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, "").unwrap();
}
fs::create_dir_all("empty-directory").unwrap();
}
fn resources_map(literal: &[(&str, &str)]) -> HashMap<String, String> {
literal
.iter()
.map(|(from, to)| (from.to_string(), to.to_string()))
.collect()
}
#[test]
#[serial_test::serial(resources)]
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(
&[
"../empty-directory".into(),
"../src/script.js".into(),
"../src/assets".into(),
"../src/index.html".into(),
"../src/sounds".into(),
"../src/textures/**/*".into(),
"*.toml".into(),
"*.conf.json".into(),
],
true,
)
.iter()
.flatten()
.collect::<Vec<_>>();
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"),
(
"../src/textures/ground/earth.tex",
"_up_/src/textures/ground/earth.tex",
),
(
"../src/textures/ground/sand.tex",
"_up_/src/textures/ground/sand.tex",
),
("../src/textures/water.tex", "_up_/src/textures/water.tex"),
("../src/textures/fire.tex", "_up_/src/textures/fire.tex"),
("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:?}");
}
}
}
#[test]
#[serial_test::serial(resources)]
fn resource_paths_iter_rerun_if_changed() {
setup_test_dirs();
let dir = std::env::current_dir().unwrap().join("src-tauri");
let _ = std::env::set_current_dir(dir);
let patterns = [
"../src/script.js".into(),
"../src/assets".into(),
"../src/textures/**/*".into(),
"*.toml".into(),
];
let mut resources = ResourcePaths::new(&patterns, true).iter();
for resource in resources.by_ref() {
resource.unwrap();
}
assert_eq!(
resources.rerun_if_changed(),
&[
normalize(Path::new("../src/script.js")),
normalize(Path::new("../src/assets")),
normalize(Path::new("../src/textures")),
PathBuf::from("."),
]
);
}
#[test]
#[serial_test::serial(resources)]
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::<Vec<_>>();
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:?}");
}
}
}
#[test]
#[serial_test::serial(resources)]
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(
&resources_map(&[
("../src/script.js", "main.js"),
("../src/assets", ""),
("../src/index.html", "frontend/index.html"),
("../src/sounds", "voices"),
("../src/textures/*", "textures"),
("../src/tiles/**/*", "tiles"),
("*.toml", ""),
("*.conf.json", "json"),
("./some-folder/", "some-target-folder/"),
("../non-existent-file", "asd"), ("../non/*", "asd"), ]),
true,
)
.iter()
.flatten()
.collect::<Vec<_>>();
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"),
(
"some-folder/some-file.txt",
"some-target-folder/some-file.txt",
),
]);
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(resources)]
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(
&resources_map(&[
("../src/script.js", "main.js"),
("../src/assets", ""),
("../src/index.html", "frontend/index.html"),
("../src/sounds", "voices"),
("*.toml", ""),
("*.conf.json", "json"),
]),
false,
)
.iter()
.flatten()
.collect::<Vec<_>>();
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:?}");
}
}
}
#[test]
#[serial_test::serial(resources)]
fn resource_paths_iter_slice_with_base_dir() {
setup_test_dirs();
let base_dir = std::env::current_dir().unwrap().join("src-tauri");
let resources = ResourcePaths::new(
&[
"../src/script.js".into(),
"../src/assets".into(),
"../src/textures/**/*".into(),
"*.toml".into(),
"some-folder".into(),
],
true,
)
.with_base_dir(&base_dir)
.iter()
.flatten()
.collect::<Vec<_>>();
let expected: Vec<Resource> = [
("../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/textures/ground/earth.tex",
"_up_/src/textures/ground/earth.tex",
),
(
"../src/textures/ground/sand.tex",
"_up_/src/textures/ground/sand.tex",
),
("../src/textures/water.tex", "_up_/src/textures/water.tex"),
("../src/textures/fire.tex", "_up_/src/textures/fire.tex"),
("Cargo.toml", "Cargo.toml"),
("Tauri.toml", "Tauri.toml"),
("some-folder/some-file.txt", "some-folder/some-file.txt"),
]
.iter()
.map(|(path, target)| Resource {
path: base_dir.join(path),
target: Path::new(target).components().collect(),
})
.collect();
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(resources)]
fn resource_paths_iter_map_with_base_dir() {
setup_test_dirs();
let base_dir = std::env::current_dir().unwrap().join("src-tauri");
let resources = ResourcePaths::from_map(
&resources_map(&[
("../src/script.js", "main.js"),
("../src/assets", ""),
("../src/sounds", "voices"),
("../src/textures/*", "textures"),
("*.conf.json", "json"),
("some-folder", "some-target-folder"),
("Cargo.toml", ""),
]),
true,
)
.with_base_dir(&base_dir)
.iter()
.flatten()
.collect::<Vec<_>>();
let expected: Vec<Resource> = [
("../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/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"),
("tauri.conf.json", "json/tauri.conf.json"),
(
"some-folder/some-file.txt",
"some-target-folder/some-file.txt",
),
("Cargo.toml", "Cargo.toml"),
]
.iter()
.map(|(path, target)| Resource {
path: base_dir.join(path),
target: Path::new(target).components().collect(),
})
.collect();
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(resources)]
fn resource_paths_errors() {
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(
&resources_map(&[
("../non-existent-file", "file"),
("../non-existent-dir", "dir"),
("../src", "dir2"),
("../non-existent-glob-dir/*", "glob"),
("../src/dir/*", "dir3"),
]),
false,
)
.iter()
.collect::<Vec<_>>();
assert_eq!(resources.len(), 5);
assert!(resources.iter().all(|r| r.is_err()));
assert!(
resources
.iter()
.any(|r| matches!(r, Err(crate::Error::ResourcePathNotFound(_))))
);
assert_eq!(
resources
.iter()
.filter(|r| matches!(r, Err(crate::Error::ResourcePathNotFound(_))))
.count(),
2
);
assert!(
resources
.iter()
.any(|r| matches!(r, Err(crate::Error::NotAllowedToWalkDir(_))))
);
assert_eq!(
resources
.iter()
.filter(|r| matches!(r, Err(crate::Error::NotAllowedToWalkDir(_))))
.count(),
1
);
assert!(
resources
.iter()
.any(|r| matches!(r, Err(crate::Error::GlobPathNotFound(_))))
);
assert_eq!(
resources
.iter()
.filter(|r| matches!(r, Err(crate::Error::GlobPathNotFound(_))))
.count(),
2
);
}
}