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).map_err(|error| EmbeddedAssetsError::AssetRead {
184 path: path.to_path_buf(),
185 error,
186 })?,
187 );
188 let hash = hasher.finalize();
189 self.scripts.push(format!(
190 "'sha256-{}'",
191 base64::engine::general_purpose::STANDARD.encode(hash)
192 ));
193 }
194 }
195
196 Ok(())
197 }
198}
199
200#[derive(Default)]
202pub struct AssetOptions {
203 pub(crate) csp: bool,
204 pub(crate) pattern: PatternKind,
205 pub(crate) freeze_prototype: bool,
206 pub(crate) dangerous_disable_asset_csp_modification: DisabledCspModificationKind,
207 #[cfg(feature = "isolation")]
208 pub(crate) isolation_schema: String,
209}
210
211impl AssetOptions {
212 pub fn new(pattern: PatternKind) -> Self {
214 Self {
215 csp: false,
216 pattern,
217 freeze_prototype: false,
218 dangerous_disable_asset_csp_modification: DisabledCspModificationKind::Flag(false),
219 #[cfg(feature = "isolation")]
220 isolation_schema: format!("isolation-{}", uuid::Uuid::new_v4()),
221 }
222 }
223
224 #[must_use]
226 pub fn with_csp(mut self) -> Self {
227 self.csp = true;
228 self
229 }
230
231 #[must_use]
233 pub fn freeze_prototype(mut self, freeze: bool) -> Self {
234 self.freeze_prototype = freeze;
235 self
236 }
237
238 pub fn dangerous_disable_asset_csp_modification(
240 mut self,
241 dangerous_disable_asset_csp_modification: DisabledCspModificationKind,
242 ) -> Self {
243 self.dangerous_disable_asset_csp_modification = dangerous_disable_asset_csp_modification;
244 self
245 }
246}
247
248impl EmbeddedAssets {
249 pub fn new(
253 input: impl Into<EmbeddedAssetsInput>,
254 options: &AssetOptions,
255 mut map: impl FnMut(
256 &AssetKey,
257 &Path,
258 &mut Vec<u8>,
259 &mut CspHashes,
260 ) -> Result<(), EmbeddedAssetsError>,
261 ) -> Result<Self, EmbeddedAssetsError> {
262 let RawEmbeddedAssets { paths, csp_hashes } = RawEmbeddedAssets::new(input.into(), options)?;
264
265 struct CompressState {
266 csp_hashes: CspHashes,
267 assets: HashMap<AssetKey, (PathBuf, PathBuf)>,
268 }
269
270 let CompressState { assets, csp_hashes } = paths.into_iter().try_fold(
271 CompressState {
272 csp_hashes,
273 assets: HashMap::new(),
274 },
275 move |mut state, (prefix, entry)| {
276 let (key, asset) =
277 Self::compress_file(&prefix, entry.path(), &mut map, &mut state.csp_hashes)?;
278 state.assets.insert(key, asset);
279 Result::<_, EmbeddedAssetsError>::Ok(state)
280 },
281 )?;
282
283 Ok(Self { assets, csp_hashes })
284 }
285
286 #[cfg(feature = "compression")]
288 fn compression_settings() -> BrotliEncoderParams {
289 let mut settings = BrotliEncoderParams::default();
290
291 if cfg!(debug_assertions) {
295 settings.quality = 2
296 } else {
297 settings.quality = 9
298 }
299
300 settings
301 }
302
303 fn compress_file(
305 prefix: &Path,
306 path: &Path,
307 map: &mut impl FnMut(
308 &AssetKey,
309 &Path,
310 &mut Vec<u8>,
311 &mut CspHashes,
312 ) -> Result<(), EmbeddedAssetsError>,
313 csp_hashes: &mut CspHashes,
314 ) -> Result<Asset, EmbeddedAssetsError> {
315 let mut input = std::fs::read(path).map_err(|error| EmbeddedAssetsError::AssetRead {
316 path: path.to_owned(),
317 error,
318 })?;
319
320 let key = path
322 .strip_prefix(prefix)
323 .map(AssetKey::from) .map_err(|_| EmbeddedAssetsError::PrefixInvalid {
325 prefix: prefix.to_owned(),
326 path: path.to_owned(),
327 })?;
328
329 map(&key, path, &mut input, csp_hashes)?;
331
332 let out_dir = std::env::var("OUT_DIR")
334 .map_err(|_| EmbeddedAssetsError::OutDir)
335 .map(PathBuf::from)
336 .and_then(|p| p.canonicalize().map_err(|_| EmbeddedAssetsError::OutDir))
337 .map(|p| p.join(TARGET_PATH))?;
338
339 std::fs::create_dir_all(&out_dir).map_err(|_| EmbeddedAssetsError::OutDir)?;
341
342 let hash = crate::checksum(&input).map_err(EmbeddedAssetsError::Hex)?;
344
345 let out_path = if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
347 out_dir.join(format!("{hash}.{ext}"))
348 } else {
349 out_dir.join(hash)
350 };
351
352 if !out_path.exists() {
354 #[allow(unused_mut)]
355 let mut out_file =
356 File::create(&out_path).map_err(|error| EmbeddedAssetsError::AssetWrite {
357 path: out_path.clone(),
358 error,
359 })?;
360
361 #[cfg(not(feature = "compression"))]
362 {
363 use std::io::Write;
364 out_file
365 .write_all(&input)
366 .map_err(|error| EmbeddedAssetsError::AssetWrite {
367 path: path.to_owned(),
368 error,
369 })?;
370 }
371
372 #[cfg(feature = "compression")]
373 {
374 let mut input = std::io::Cursor::new(input);
375 brotli::BrotliCompress(&mut input, &mut out_file, &Self::compression_settings()).map_err(
377 |error| EmbeddedAssetsError::AssetWrite {
378 path: path.to_owned(),
379 error,
380 },
381 )?;
382 }
383 }
384
385 Ok((key, (path.into(), out_path)))
386 }
387}
388
389impl ToTokens for EmbeddedAssets {
390 fn to_tokens(&self, tokens: &mut TokenStream) {
391 let mut assets = TokenStream::new();
392 for (key, (input, output)) in &self.assets {
393 let key: &str = key.as_ref();
394 let input = input.display().to_string();
395 let output = output.display().to_string();
396
397 assets.append_all(quote!(#key => {
399 const _: &[u8] = include_bytes!(#input);
400 include_bytes!(#output)
401 },));
402 }
403
404 let mut global_hashes = TokenStream::new();
405 for script_hash in &self.csp_hashes.scripts {
406 let hash = script_hash.as_str();
407 global_hashes.append_all(quote!(CspHash::Script(#hash),));
408 }
409
410 for style_hash in &self.csp_hashes.styles {
411 let hash = style_hash.as_str();
412 global_hashes.append_all(quote!(CspHash::Style(#hash),));
413 }
414
415 let mut html_hashes = TokenStream::new();
416 for (path, hashes) in &self.csp_hashes.inline_scripts {
417 let key = path.as_str();
418 let mut value = TokenStream::new();
419 for script_hash in hashes {
420 let hash = script_hash.as_str();
421 value.append_all(quote!(CspHash::Script(#hash),));
422 }
423 html_hashes.append_all(quote!(#key => &[#value],));
424 }
425
426 tokens.append_all(quote! {{
428 #[allow(unused_imports)]
429 use ::tauri::utils::assets::{CspHash, EmbeddedAssets, phf, phf::phf_map};
430 EmbeddedAssets::new(phf_map! { #assets }, &[#global_hashes], phf_map! { #html_hashes })
431 }});
432 }
433}
434
435pub(crate) fn ensure_out_dir() -> EmbeddedAssetsResult<PathBuf> {
436 let out_dir = std::env::var("OUT_DIR")
437 .map_err(|_| EmbeddedAssetsError::OutDir)
438 .map(PathBuf::from)
439 .and_then(|p| p.canonicalize().map_err(|_| EmbeddedAssetsError::OutDir))?;
440
441 std::fs::create_dir_all(&out_dir).map_err(|_| EmbeddedAssetsError::OutDir)?;
443 Ok(out_dir)
444}