1use kuchiki::traits::*;
6use proc_macro2::TokenStream;
7use quote::{quote, ToTokens, TokenStreamExt};
8use regex::RegexSet;
9use std::{
10 collections::HashMap,
11 ffi::OsStr,
12 fs::File,
13 path::{Path, PathBuf},
14};
15use tauri_utils::{
16 assets::AssetKey,
17 html::{inject_csp, inject_invoke_key_token},
18};
19use thiserror::Error;
20use walkdir::WalkDir;
21
22const TARGET_PATH: &str = "tauri-codegen-assets";
24
25const MULTI_HASH_SIZE_LIMIT: usize = 131_072; type Asset = (AssetKey, (PathBuf, PathBuf));
30
31#[derive(Debug, Error)]
33#[non_exhaustive]
34pub enum EmbeddedAssetsError {
35 #[error("failed to read asset at {path} because {error}")]
36 AssetRead {
37 path: PathBuf,
38 error: std::io::Error,
39 },
40
41 #[error("failed to write asset from {path} to Vec<u8> because {error}")]
42 AssetWrite {
43 path: PathBuf,
44 error: std::io::Error,
45 },
46
47 #[error("invalid prefix {prefix} used while including path {path}")]
48 PrefixInvalid { prefix: PathBuf, path: PathBuf },
49
50 #[error("failed to walk directory {path} because {error}")]
51 Walkdir {
52 path: PathBuf,
53 error: walkdir::Error,
54 },
55
56 #[error("OUT_DIR env var is not set, do you have a build script?")]
57 OutDir,
58}
59
60#[derive(Default)]
70pub struct EmbeddedAssets(HashMap<AssetKey, (PathBuf, PathBuf)>);
71
72#[derive(Default)]
74pub struct AssetOptions {
75 csp: Option<String>,
76}
77
78impl AssetOptions {
79 pub fn new() -> Self {
81 Self::default()
82 }
83
84 pub fn csp(mut self, csp: String) -> Self {
86 self.csp.replace(csp);
87 self
88 }
89}
90
91impl EmbeddedAssets {
92 pub fn new(path: &Path, options: AssetOptions) -> Result<Self, EmbeddedAssetsError> {
94 WalkDir::new(&path)
95 .follow_links(true)
96 .into_iter()
97 .filter_map(|entry| match entry {
98 Ok(entry) if entry.file_type().is_dir() => None,
100
101 Ok(entry) => Some(Self::compress_file(path, entry.path(), &options)),
103
104 Err(error) => Some(Err(EmbeddedAssetsError::Walkdir {
106 path: path.to_owned(),
107 error,
108 })),
109 })
110 .collect::<Result<_, _>>()
111 .map(Self)
112 }
113
114 pub fn load_paths(
116 paths: Vec<PathBuf>,
117 options: AssetOptions,
118 ) -> Result<Self, EmbeddedAssetsError> {
119 Ok(Self(
120 paths
121 .iter()
122 .map(|path| {
123 let is_file = path.is_file();
124 WalkDir::new(&path)
125 .follow_links(true)
126 .into_iter()
127 .filter_map(|entry| {
128 match entry {
129 Ok(entry) if entry.file_type().is_dir() => None,
131
132 Ok(entry) => Some(Self::compress_file(
134 if is_file {
135 path.parent().unwrap()
136 } else {
137 path
138 },
139 entry.path(),
140 &options,
141 )),
142
143 Err(error) => Some(Err(EmbeddedAssetsError::Walkdir {
145 path: path.to_path_buf(),
146 error,
147 })),
148 }
149 })
150 .collect::<Result<Vec<Asset>, _>>()
151 })
152 .flatten()
153 .flatten()
154 .collect::<_>(),
155 ))
156 }
157
158 fn compression_level() -> i32 {
160 let levels = zstd::compression_level_range();
161 if cfg!(debug_assertions) {
162 *levels.start()
163 } else {
164 *levels.end()
165 }
166 }
167
168 fn compress_file(
170 prefix: &Path,
171 path: &Path,
172 options: &AssetOptions,
173 ) -> Result<Asset, EmbeddedAssetsError> {
174 let mut input = std::fs::read(path).map_err(|error| EmbeddedAssetsError::AssetRead {
175 path: path.to_owned(),
176 error,
177 })?;
178 if path.extension() == Some(OsStr::new("html")) {
179 let mut document = kuchiki::parse_html().one(String::from_utf8_lossy(&input).into_owned());
180 if let Some(csp) = &options.csp {
181 inject_csp(&mut document, csp);
182 }
183 inject_invoke_key_token(&mut document);
184 input = document.to_string().as_bytes().to_vec();
185 } else {
186 let is_javascript = ["js", "cjs", "mjs"]
187 .iter()
188 .any(|e| path.extension() == Some(OsStr::new(e)));
189 if is_javascript {
190 let js = String::from_utf8_lossy(&input).into_owned();
191 input = if RegexSet::new(&[
192 "import\\{",
194 "import \\{",
195 "import\\*",
196 "import \\*",
197 "import (\"|');?$",
198 "import\\(",
199 "import (.|\n)+ from (\"|')([A-Za-z/\\.@-]+)(\"|')",
200 "export\\{",
202 "export \\{",
203 "export\\*",
204 "export \\*",
205 "export (default|class|let|const|function)",
206 ])
207 .unwrap()
208 .is_match(&js)
209 {
210 format!(
211 r#"
212 const __TAURI_INVOKE_KEY__ = __TAURI__INVOKE_KEY_TOKEN__;
213 {}
214 "#,
215 js
216 )
217 .as_bytes()
218 .to_vec()
219 } else {
220 format!(
221 r#"(function () {{
222 const __TAURI_INVOKE_KEY__ = __TAURI__INVOKE_KEY_TOKEN__;
223 {}
224 }})()"#,
225 js
226 )
227 .as_bytes()
228 .to_vec()
229 };
230 }
231 }
232
233 let out_dir = std::env::var("OUT_DIR")
235 .map_err(|_| EmbeddedAssetsError::OutDir)
236 .map(PathBuf::from)
237 .and_then(|p| p.canonicalize().map_err(|_| EmbeddedAssetsError::OutDir))
238 .map(|p| p.join(TARGET_PATH))?;
239
240 std::fs::create_dir_all(&out_dir).map_err(|_| EmbeddedAssetsError::OutDir)?;
242
243 let hash = {
245 let mut hasher = blake3::Hasher::new();
246 if input.len() < MULTI_HASH_SIZE_LIMIT {
247 hasher.update(&input);
248 } else {
249 hasher.update_rayon(&input);
250 }
251 hasher.finalize().to_hex()
252 };
253
254 let out_path = if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
256 out_dir.join(format!("{}.{}", hash, ext))
257 } else {
258 out_dir.join(hash.to_string())
259 };
260
261 if !out_path.exists() {
263 let out_file = File::create(&out_path).map_err(|error| EmbeddedAssetsError::AssetWrite {
264 path: out_path.clone(),
265 error,
266 })?;
267
268 zstd::stream::copy_encode(&*input, out_file, Self::compression_level()).map_err(|error| {
270 EmbeddedAssetsError::AssetWrite {
271 path: path.to_owned(),
272 error,
273 }
274 })?;
275 }
276
277 let key = path
279 .strip_prefix(prefix)
280 .map(AssetKey::from) .map_err(|_| EmbeddedAssetsError::PrefixInvalid {
282 prefix: prefix.to_owned(),
283 path: path.to_owned(),
284 })?;
285
286 Ok((key, (path.into(), out_path)))
287 }
288}
289
290impl ToTokens for EmbeddedAssets {
291 fn to_tokens(&self, tokens: &mut TokenStream) {
292 let mut map = TokenStream::new();
293 for (key, (input, output)) in &self.0 {
294 let key: &str = key.as_ref();
295 let input = input.display().to_string();
296 let output = output.display().to_string();
297
298 map.append_all(quote!(#key => {
300 const _: &[u8] = include_bytes!(#input);
301 include_bytes!(#output)
302 },));
303 }
304
305 tokens.append_all(quote! {{
307 use ::tauri::api::assets::{EmbeddedAssets, phf, phf::phf_map};
308 EmbeddedAssets::from_zstd(phf_map! { #map })
309 }});
310 }
311}