embedded_assets.rs 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. // Copyright 2019-2021 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use kuchiki::traits::*;
  5. use proc_macro2::TokenStream;
  6. use quote::{quote, ToTokens, TokenStreamExt};
  7. use regex::RegexSet;
  8. use std::{
  9. collections::HashMap,
  10. ffi::OsStr,
  11. fs::File,
  12. path::{Path, PathBuf},
  13. };
  14. use tauri_utils::{
  15. assets::AssetKey,
  16. html::{inject_csp, inject_invoke_key_token},
  17. };
  18. use thiserror::Error;
  19. use walkdir::WalkDir;
  20. /// The subdirectory inside the target directory we want to place assets.
  21. const TARGET_PATH: &str = "tauri-codegen-assets";
  22. /// The minimum size needed for the hasher to use multiple threads.
  23. const MULTI_HASH_SIZE_LIMIT: usize = 131_072; // 128KiB
  24. /// (key, (original filepath, compressed bytes))
  25. type Asset = (AssetKey, (PathBuf, PathBuf));
  26. /// All possible errors while reading and compressing an [`EmbeddedAssets`] directory
  27. #[derive(Debug, Error)]
  28. #[non_exhaustive]
  29. pub enum EmbeddedAssetsError {
  30. #[error("failed to read asset at {path} because {error}")]
  31. AssetRead {
  32. path: PathBuf,
  33. error: std::io::Error,
  34. },
  35. #[error("failed to write asset from {path} to Vec<u8> because {error}")]
  36. AssetWrite {
  37. path: PathBuf,
  38. error: std::io::Error,
  39. },
  40. #[error("invalid prefix {prefix} used while including path {path}")]
  41. PrefixInvalid { prefix: PathBuf, path: PathBuf },
  42. #[error("failed to walk directory {path} because {error}")]
  43. Walkdir {
  44. path: PathBuf,
  45. error: walkdir::Error,
  46. },
  47. #[error("OUT_DIR env var is not set, do you have a build script?")]
  48. OutDir,
  49. }
  50. /// Represent a directory of assets that are compressed and embedded.
  51. ///
  52. /// This is the compile time generation of [`tauri_utils::assets::Assets`] from a directory. Assets
  53. /// from the directory are added as compiler dependencies by dummy including the original,
  54. /// uncompressed assets.
  55. ///
  56. /// The assets are compressed during this runtime, and can only be represented as a [`TokenStream`]
  57. /// through [`ToTokens`]. The generated code is meant to be injected into an application to include
  58. /// the compressed assets in that application's binary.
  59. #[derive(Default)]
  60. pub struct EmbeddedAssets(HashMap<AssetKey, (PathBuf, PathBuf)>);
  61. /// Options used to embed assets.
  62. #[derive(Default)]
  63. pub struct AssetOptions {
  64. csp: Option<String>,
  65. }
  66. impl AssetOptions {
  67. /// Creates the default asset options.
  68. pub fn new() -> Self {
  69. Self::default()
  70. }
  71. /// Sets the content security policy to add to HTML files.
  72. pub fn csp(mut self, csp: String) -> Self {
  73. self.csp.replace(csp);
  74. self
  75. }
  76. }
  77. impl EmbeddedAssets {
  78. /// Compress a directory of assets, ready to be generated into a [`tauri_utils::assets::Assets`].
  79. pub fn new(path: &Path, options: AssetOptions) -> Result<Self, EmbeddedAssetsError> {
  80. WalkDir::new(&path)
  81. .follow_links(true)
  82. .into_iter()
  83. .filter_map(|entry| match entry {
  84. // we only serve files, not directory listings
  85. Ok(entry) if entry.file_type().is_dir() => None,
  86. // compress all files encountered
  87. Ok(entry) => Some(Self::compress_file(path, entry.path(), &options)),
  88. // pass down error through filter to fail when encountering any error
  89. Err(error) => Some(Err(EmbeddedAssetsError::Walkdir {
  90. path: path.to_owned(),
  91. error,
  92. })),
  93. })
  94. .collect::<Result<_, _>>()
  95. .map(Self)
  96. }
  97. /// Compress a list of files and directories.
  98. pub fn load_paths(
  99. paths: Vec<PathBuf>,
  100. options: AssetOptions,
  101. ) -> Result<Self, EmbeddedAssetsError> {
  102. Ok(Self(
  103. paths
  104. .iter()
  105. .map(|path| {
  106. let is_file = path.is_file();
  107. WalkDir::new(&path)
  108. .follow_links(true)
  109. .into_iter()
  110. .filter_map(|entry| {
  111. match entry {
  112. // we only serve files, not directory listings
  113. Ok(entry) if entry.file_type().is_dir() => None,
  114. // compress all files encountered
  115. Ok(entry) => Some(Self::compress_file(
  116. if is_file {
  117. path.parent().unwrap()
  118. } else {
  119. path
  120. },
  121. entry.path(),
  122. &options,
  123. )),
  124. // pass down error through filter to fail when encountering any error
  125. Err(error) => Some(Err(EmbeddedAssetsError::Walkdir {
  126. path: path.to_path_buf(),
  127. error,
  128. })),
  129. }
  130. })
  131. .collect::<Result<Vec<Asset>, _>>()
  132. })
  133. .flatten()
  134. .flatten()
  135. .collect::<_>(),
  136. ))
  137. }
  138. /// Use highest compression level for release, the fastest one for everything else
  139. fn compression_level() -> i32 {
  140. let levels = zstd::compression_level_range();
  141. if cfg!(debug_assertions) {
  142. *levels.start()
  143. } else {
  144. *levels.end()
  145. }
  146. }
  147. /// Compress a file and spit out the information in a [`HashMap`] friendly form.
  148. fn compress_file(
  149. prefix: &Path,
  150. path: &Path,
  151. options: &AssetOptions,
  152. ) -> Result<Asset, EmbeddedAssetsError> {
  153. let mut input = std::fs::read(path).map_err(|error| EmbeddedAssetsError::AssetRead {
  154. path: path.to_owned(),
  155. error,
  156. })?;
  157. if path.extension() == Some(OsStr::new("html")) {
  158. let mut document = kuchiki::parse_html().one(String::from_utf8_lossy(&input).into_owned());
  159. if let Some(csp) = &options.csp {
  160. inject_csp(&mut document, csp);
  161. }
  162. inject_invoke_key_token(&mut document);
  163. input = document.to_string().as_bytes().to_vec();
  164. } else {
  165. let is_javascript = ["js", "cjs", "mjs"]
  166. .iter()
  167. .any(|e| path.extension() == Some(OsStr::new(e)));
  168. if is_javascript {
  169. let js = String::from_utf8_lossy(&input).into_owned();
  170. input = if RegexSet::new(&[
  171. // import keywords
  172. "import\\{",
  173. "import \\{",
  174. "import\\*",
  175. "import \\*",
  176. "import (\"|');?$",
  177. "import\\(",
  178. "import (.|\n)+ from (\"|')([A-Za-z/\\.@-]+)(\"|')",
  179. // export keywords
  180. "export\\{",
  181. "export \\{",
  182. "export\\*",
  183. "export \\*",
  184. "export (default|class|let|const|function|async)",
  185. ])
  186. .unwrap()
  187. .is_match(&js)
  188. {
  189. format!(
  190. r#"
  191. const __TAURI_INVOKE_KEY__ = __TAURI__INVOKE_KEY_TOKEN__;
  192. {}
  193. "#,
  194. js
  195. )
  196. .as_bytes()
  197. .to_vec()
  198. } else {
  199. format!(
  200. r#"(function () {{
  201. const __TAURI_INVOKE_KEY__ = __TAURI__INVOKE_KEY_TOKEN__;
  202. {}
  203. }})()"#,
  204. js
  205. )
  206. .as_bytes()
  207. .to_vec()
  208. };
  209. }
  210. }
  211. // we must canonicalize the base of our paths to allow long paths on windows
  212. let out_dir = std::env::var("OUT_DIR")
  213. .map_err(|_| EmbeddedAssetsError::OutDir)
  214. .map(PathBuf::from)
  215. .and_then(|p| p.canonicalize().map_err(|_| EmbeddedAssetsError::OutDir))
  216. .map(|p| p.join(TARGET_PATH))?;
  217. // make sure that our output directory is created
  218. std::fs::create_dir_all(&out_dir).map_err(|_| EmbeddedAssetsError::OutDir)?;
  219. // get a hash of the input - allows for caching existing files
  220. let hash = {
  221. let mut hasher = blake3::Hasher::new();
  222. if input.len() < MULTI_HASH_SIZE_LIMIT {
  223. hasher.update(&input);
  224. } else {
  225. hasher.update_rayon(&input);
  226. }
  227. hasher.finalize().to_hex()
  228. };
  229. // use the content hash to determine filename, keep extensions that exist
  230. let out_path = if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
  231. out_dir.join(format!("{}.{}", hash, ext))
  232. } else {
  233. out_dir.join(hash.to_string())
  234. };
  235. // only compress and write to the file if it doesn't already exist.
  236. if !out_path.exists() {
  237. let out_file = File::create(&out_path).map_err(|error| EmbeddedAssetsError::AssetWrite {
  238. path: out_path.clone(),
  239. error,
  240. })?;
  241. // entirely write input to the output file path with compression
  242. zstd::stream::copy_encode(&*input, out_file, Self::compression_level()).map_err(|error| {
  243. EmbeddedAssetsError::AssetWrite {
  244. path: path.to_owned(),
  245. error,
  246. }
  247. })?;
  248. }
  249. // get a key to the asset path without the asset directory prefix
  250. let key = path
  251. .strip_prefix(prefix)
  252. .map(AssetKey::from) // format the path for use in assets
  253. .map_err(|_| EmbeddedAssetsError::PrefixInvalid {
  254. prefix: prefix.to_owned(),
  255. path: path.to_owned(),
  256. })?;
  257. Ok((key, (path.into(), out_path)))
  258. }
  259. }
  260. impl ToTokens for EmbeddedAssets {
  261. fn to_tokens(&self, tokens: &mut TokenStream) {
  262. let mut map = TokenStream::new();
  263. for (key, (input, output)) in &self.0 {
  264. let key: &str = key.as_ref();
  265. let input = input.display().to_string();
  266. let output = output.display().to_string();
  267. // add original asset as a compiler dependency, rely on dead code elimination to clean it up
  268. map.append_all(quote!(#key => {
  269. const _: &[u8] = include_bytes!(#input);
  270. include_bytes!(#output)
  271. },));
  272. }
  273. // we expect phf related items to be in path when generating the path code
  274. tokens.append_all(quote! {{
  275. use ::tauri::utils::assets::{EmbeddedAssets, phf, phf::phf_map};
  276. EmbeddedAssets::from_zstd(phf_map! { #map })
  277. }});
  278. }
  279. }