1use base64::Engine;
6use proc_macro2::TokenStream;
7use quote::{quote, ToTokens, TokenStreamExt};
8use sha2::{Digest, Sha256};
9use std::{
10 collections::HashMap,
11 fs::File,
12 path::{Path, PathBuf},
13};
14use tauri_utils::config::PatternKind;
15use tauri_utils::{assets::AssetKey, config::DisabledCspModificationKind};
16use thiserror::Error;
17use walkdir::{DirEntry, WalkDir};
18
19#[cfg(feature = "compression")]
20use brotli::enc::backward_references::BrotliEncoderParams;
21
22const TARGET_PATH: &str = "tauri-codegen-assets";
24
25type Asset = (AssetKey, (PathBuf, PathBuf));
27
28#[derive(Debug, Error)]
30#[non_exhaustive]
31pub enum EmbeddedAssetsError {
32 #[error("failed to read asset at {path} because {error}")]
33 AssetRead {
34 path: PathBuf,
35 error: std::io::Error,
36 },
37
38 #[error("failed to write asset from {path} to Vec<u8> because {error}")]
39 AssetWrite {
40 path: PathBuf,
41 error: std::io::Error,
42 },
43
44 #[error("failed to create hex from bytes because {0}")]
45 Hex(std::fmt::Error),
46
47 #[error("invalid prefix {prefix} used while including path {path}")]
48 PrefixInvalid { prefix: PathBuf, path: PathBuf },
49
50 #[error("invalid extension `{extension}` used for image {path}, must be `ico` or `png`")]
51 InvalidImageExtension { extension: PathBuf, path: PathBuf },
52
53 #[error("failed to walk directory {path} because {error}")]
54 Walkdir {
55 path: PathBuf,
56 error: walkdir::Error,
57 },
58
59 #[error("OUT_DIR env var is not set, do you have a build script?")]
60 OutDir,
61
62 #[error("version error: {0}")]
63 Version(#[from] semver::Error),
64}
65
66pub type EmbeddedAssetsResult<T> = Result<T, EmbeddedAssetsError>;
67
68#[derive(Default)]
78pub struct EmbeddedAssets {
79 assets: HashMap<AssetKey, (PathBuf, PathBuf)>,
80 csp_hashes: CspHashes,
81}
82
83pub struct EmbeddedAssetsInput(Vec<PathBuf>);
84
85impl From<PathBuf> for EmbeddedAssetsInput {
86 fn from(path: PathBuf) -> Self {
87 Self(vec![path])
88 }
89}
90
91impl From<Vec<PathBuf>> for EmbeddedAssetsInput {
92 fn from(paths: Vec<PathBuf>) -> Self {
93 Self(paths)
94 }
95}
96
97struct RawEmbeddedAssets {
99 paths: Vec<(PathBuf, DirEntry)>,
100 csp_hashes: CspHashes,
101}
102
103impl RawEmbeddedAssets {
104 fn new(input: EmbeddedAssetsInput, options: &AssetOptions) -> Result<Self, EmbeddedAssetsError> {
106 let mut csp_hashes = CspHashes::default();
107
108 input
109 .0
110 .into_iter()
111 .flat_map(|path| {
112 let prefix = if path.is_dir() {
113 path.clone()
114 } else {
115 path
116 .parent()
117 .expect("embedded file asset has no parent")
118 .to_path_buf()
119 };
120
121 WalkDir::new(&path)
122 .follow_links(true)
123 .contents_first(true)
124 .into_iter()
125 .map(move |entry| (prefix.clone(), entry))
126 })
127 .filter_map(|(prefix, entry)| {
128 match entry {
129 Ok(entry) if entry.file_type().is_dir() => None,
131
132 Ok(entry) => {
134 if let Err(error) = csp_hashes
135 .add_if_applicable(&entry, &options.dangerous_disable_asset_csp_modification)
136 {
137 Some(Err(error))
138 } else {
139 Some(Ok((prefix, entry)))
140 }
141 }
142
143 Err(error) => Some(Err(EmbeddedAssetsError::Walkdir {
145 path: prefix,
146 error,
147 })),
148 }
149 })
150 .collect::<Result<Vec<(PathBuf, DirEntry)>, _>>()
151 .map(|paths| Self { paths, csp_hashes })
152 }
153}
154
155#[derive(Debug, Default)]
157pub struct CspHashes {
158 pub(crate) scripts: Vec<String>,
160 pub(crate) inline_scripts: HashMap<String, Vec<String>>,
162 pub(crate) styles: Vec<String>,
164}
165
166impl CspHashes {
167 pub fn add_if_applicable(
172 &mut self,
173 entry: &DirEntry,
174 dangerous_disable_asset_csp_modification: &DisabledCspModificationKind,
175 ) -> Result<(), EmbeddedAssetsError> {
176 let path = entry.path();
177
178 if let Some("js") | Some("mjs") = path.extension().and_then(|os| os.to_str()) {
180 if dangerous_disable_asset_csp_modification.can_modify("script-src") {
181 let mut hasher = Sha256::new();
182 hasher.update(
183 &std::fs::read(path)
184 .map(|b| tauri_utils::html::normalize_script_for_csp(&b))
185 .map_err(|error| EmbeddedAssetsError::AssetRead {
186 path: path.to_path_buf(),
187 error,
188 })?,
189 );
190 let hash = hasher.finalize();
191 self.scripts.push(format!(
192 "'sha256-{}'",
193 base64::engine::general_purpose::STANDARD.encode(hash)
194 ));
195 }
196 }
197
198 Ok(())
199 }
200}
201
202#[derive(Default)]
204pub struct AssetOptions {
205 pub(crate) csp: bool,
206 pub(crate) pattern: PatternKind,
207 pub(crate) freeze_prototype: bool,
208 pub(crate) dangerous_disable_asset_csp_modification: DisabledCspModificationKind,
209 #[cfg(feature = "isolation")]
210 pub(crate) isolation_schema: String,
211}
212
213impl AssetOptions {
214 pub fn new(pattern: PatternKind) -> Self {
216 Self {
217 csp: false,
218 pattern,
219 freeze_prototype: false,
220 dangerous_disable_asset_csp_modification: DisabledCspModificationKind::Flag(false),
221 #[cfg(feature = "isolation")]
222 isolation_schema: format!("isolation-{}", uuid::Uuid::new_v4()),
223 }
224 }
225
226 #[must_use]
228 pub fn with_csp(mut self) -> Self {
229 self.csp = true;
230 self
231 }
232
233 #[must_use]
235 pub fn freeze_prototype(mut self, freeze: bool) -> Self {
236 self.freeze_prototype = freeze;
237 self
238 }
239
240 pub fn dangerous_disable_asset_csp_modification(
242 mut self,
243 dangerous_disable_asset_csp_modification: DisabledCspModificationKind,
244 ) -> Self {
245 self.dangerous_disable_asset_csp_modification = dangerous_disable_asset_csp_modification;
246 self
247 }
248}
249
250impl EmbeddedAssets {
251 pub fn new(
255 input: impl Into<EmbeddedAssetsInput>,
256 options: &AssetOptions,
257 mut map: impl FnMut(
258 &AssetKey,
259 &Path,
260 &mut Vec<u8>,
261 &mut CspHashes,
262 ) -> Result<(), EmbeddedAssetsError>,
263 ) -> Result<Self, EmbeddedAssetsError> {
264 let RawEmbeddedAssets { paths, csp_hashes } = RawEmbeddedAssets::new(input.into(), options)?;
266
267 struct CompressState {
268 csp_hashes: CspHashes,
269 assets: HashMap<AssetKey, (PathBuf, PathBuf)>,
270 }
271
272 let CompressState { assets, csp_hashes } = paths.into_iter().try_fold(
273 CompressState {
274 csp_hashes,
275 assets: HashMap::new(),
276 },
277 move |mut state, (prefix, entry)| {
278 let (key, asset) =
279 Self::compress_file(&prefix, entry.path(), &mut map, &mut state.csp_hashes)?;
280 state.assets.insert(key, asset);
281 Result::<_, EmbeddedAssetsError>::Ok(state)
282 },
283 )?;
284
285 Ok(Self { assets, csp_hashes })
286 }
287
288 #[cfg(feature = "compression")]
290 fn compression_settings() -> BrotliEncoderParams {
291 let mut settings = BrotliEncoderParams::default();
292
293 if cfg!(debug_assertions) {
297 settings.quality = 2
298 } else {
299 settings.quality = 9
300 }
301
302 settings
303 }
304
305 fn compress_file(
307 prefix: &Path,
308 path: &Path,
309 map: &mut impl FnMut(
310 &AssetKey,
311 &Path,
312 &mut Vec<u8>,
313 &mut CspHashes,
314 ) -> Result<(), EmbeddedAssetsError>,
315 csp_hashes: &mut CspHashes,
316 ) -> Result<Asset, EmbeddedAssetsError> {
317 let mut input = std::fs::read(path).map_err(|error| EmbeddedAssetsError::AssetRead {
318 path: path.to_owned(),
319 error,
320 })?;
321
322 let key = path
324 .strip_prefix(prefix)
325 .map(AssetKey::from) .map_err(|_| EmbeddedAssetsError::PrefixInvalid {
327 prefix: prefix.to_owned(),
328 path: path.to_owned(),
329 })?;
330
331 map(&key, path, &mut input, csp_hashes)?;
333
334 let out_dir = std::env::var("OUT_DIR")
336 .map_err(|_| EmbeddedAssetsError::OutDir)
337 .map(PathBuf::from)
338 .and_then(|p| p.canonicalize().map_err(|_| EmbeddedAssetsError::OutDir))
339 .map(|p| p.join(TARGET_PATH))?;
340
341 std::fs::create_dir_all(&out_dir).map_err(|_| EmbeddedAssetsError::OutDir)?;
343
344 let hash = crate::checksum(&input).map_err(EmbeddedAssetsError::Hex)?;
346
347 let out_path = if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
349 out_dir.join(format!("{hash}.{ext}"))
350 } else {
351 out_dir.join(hash)
352 };
353
354 if !out_path.exists() {
356 #[allow(unused_mut)]
357 let mut out_file =
358 File::create(&out_path).map_err(|error| EmbeddedAssetsError::AssetWrite {
359 path: out_path.clone(),
360 error,
361 })?;
362
363 #[cfg(not(feature = "compression"))]
364 {
365 use std::io::Write;
366 out_file
367 .write_all(&input)
368 .map_err(|error| EmbeddedAssetsError::AssetWrite {
369 path: path.to_owned(),
370 error,
371 })?;
372 }
373
374 #[cfg(feature = "compression")]
375 {
376 let mut input = std::io::Cursor::new(input);
377 brotli::BrotliCompress(&mut input, &mut out_file, &Self::compression_settings()).map_err(
379 |error| EmbeddedAssetsError::AssetWrite {
380 path: path.to_owned(),
381 error,
382 },
383 )?;
384 }
385 }
386
387 Ok((key, (path.into(), out_path)))
388 }
389}
390
391impl ToTokens for EmbeddedAssets {
392 fn to_tokens(&self, tokens: &mut TokenStream) {
393 let mut assets = TokenStream::new();
394 for (key, (input, output)) in &self.assets {
395 let key: &str = key.as_ref();
396 let input = input.display().to_string();
397 let output = output.display().to_string();
398
399 assets.append_all(quote!(#key => {
401 const _: &[u8] = include_bytes!(#input);
402 include_bytes!(#output)
403 },));
404 }
405
406 let mut global_hashes = TokenStream::new();
407 for script_hash in &self.csp_hashes.scripts {
408 let hash = script_hash.as_str();
409 global_hashes.append_all(quote!(CspHash::Script(#hash),));
410 }
411
412 for style_hash in &self.csp_hashes.styles {
413 let hash = style_hash.as_str();
414 global_hashes.append_all(quote!(CspHash::Style(#hash),));
415 }
416
417 let mut html_hashes = TokenStream::new();
418 for (path, hashes) in &self.csp_hashes.inline_scripts {
419 let key = path.as_str();
420 let mut value = TokenStream::new();
421 for script_hash in hashes {
422 let hash = script_hash.as_str();
423 value.append_all(quote!(CspHash::Script(#hash),));
424 }
425 html_hashes.append_all(quote!(#key => &[#value],));
426 }
427
428 tokens.append_all(quote! {{
430 #[allow(unused_imports)]
431 use ::tauri::utils::assets::{CspHash, EmbeddedAssets, phf, phf::phf_map};
432 EmbeddedAssets::new(phf_map! { #assets }, &[#global_hashes], phf_map! { #html_hashes })
433 }});
434 }
435}
436
437pub(crate) fn ensure_out_dir() -> EmbeddedAssetsResult<PathBuf> {
438 let out_dir = std::env::var("OUT_DIR")
439 .map_err(|_| EmbeddedAssetsError::OutDir)
440 .map(PathBuf::from)
441 .and_then(|p| p.canonicalize().map_err(|_| EmbeddedAssetsError::OutDir))?;
442
443 std::fs::create_dir_all(&out_dir).map_err(|_| EmbeddedAssetsError::OutDir)?;
445 Ok(out_dir)
446}