1use std::collections::BTreeMap;
6use std::convert::identity;
7use std::path::{Path, PathBuf};
8use std::{ffi::OsStr, str::FromStr};
9
10use crate::{
11 embedded_assets::{
12 AssetOptions, CspHashes, EmbeddedAssets, EmbeddedAssetsResult, ensure_out_dir,
13 },
14 image::CachedIcon,
15};
16use base64::Engine;
17use proc_macro2::TokenStream;
18use quote::quote;
19use sha2::{Digest, Sha256};
20use syn::Expr;
21use tauri_utils::{
22 acl::{
23 ACL_MANIFESTS_FILE_NAME, CAPABILITIES_FILE_NAME, get_capabilities, manifest::Manifest,
24 resolved::Resolved,
25 },
26 assets::AssetKey,
27 config::{Config, FrontendDist, PatternKind},
28 html2::{Document, inject_nonce_token, parse_doc, serialize_doc},
29 platform::Target,
30 tokens::{map_lit, str_lit},
31};
32
33pub struct ContextData {
35 pub dev: bool,
36 pub config: Config,
37 pub config_parent: PathBuf,
38 pub root: TokenStream,
39 pub capabilities: Option<Vec<PathBuf>>,
41 pub assets: Option<Expr>,
43 pub test: bool,
45}
46
47fn inject_script_hashes(document: &Document, key: &AssetKey, csp_hashes: &mut CspHashes) {
48 let script_elements = document.select("script:not(:empty)");
49
50 let scripts = script_elements
51 .iter()
52 .map(|element| {
53 let script = tauri_utils::html2::normalize_script_for_csp(element.text().as_bytes());
54 let script_hash = Sha256::digest(script);
55 let hash_base64 = base64::engine::general_purpose::STANDARD.encode(script_hash);
56
57 format!("'sha256-{hash_base64}'")
58 })
59 .collect::<Vec<_>>();
60
61 csp_hashes
62 .inline_scripts
63 .entry(key.clone().into())
64 .or_default()
65 .extend(scripts);
66}
67
68fn map_core_assets(
69 options: &AssetOptions,
70) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> EmbeddedAssetsResult<()> + use<> {
71 let csp = options.csp;
72 let dangerous_disable_asset_csp_modification =
73 options.dangerous_disable_asset_csp_modification.clone();
74 move |key, path, input, csp_hashes| {
75 if path.extension() == Some(OsStr::new("html")) {
76 #[allow(clippy::collapsible_if)]
77 if csp {
78 let document = parse_doc(String::from_utf8_lossy(input).into_owned());
79
80 inject_nonce_token(&document, &dangerous_disable_asset_csp_modification);
81
82 if dangerous_disable_asset_csp_modification.can_modify("script-src") {
83 inject_script_hashes(&document, key, csp_hashes);
84 }
85
86 *input = serialize_doc(&document);
87 }
88 }
89 Ok(())
90 }
91}
92
93#[cfg(feature = "isolation")]
94fn map_isolation(
95 _options: &AssetOptions,
96 dir: PathBuf,
97) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> EmbeddedAssetsResult<()> + use<> {
98 let mut hasher = Sha256::new();
100 hasher.update(tauri_utils::pattern::isolation::IFRAME_STYLE);
101 let hash = hasher.finalize();
102 let iframe_style_csp_hash = format!(
103 "'sha256-{}'",
104 base64::engine::general_purpose::STANDARD.encode(hash)
105 );
106
107 move |key, path, input, csp_hashes| {
108 if path.extension() == Some(OsStr::new("html")) {
109 let isolation_html = parse_doc(String::from_utf8_lossy(input).into_owned());
110
111 tauri_utils::html2::inject_codegen_isolation_script(&isolation_html);
113
114 tauri_utils::html2::inline_isolation(&isolation_html, &dir);
116
117 inject_nonce_token(
118 &isolation_html,
119 &tauri_utils::config::DisabledCspModificationKind::Flag(false),
120 );
121
122 inject_script_hashes(&isolation_html, key, csp_hashes);
123
124 csp_hashes.styles.push(iframe_style_csp_hash.clone());
125
126 *input = serialize_doc(&isolation_html)
127 }
128
129 Ok(())
130 }
131}
132
133pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult<TokenStream> {
135 let ContextData {
136 dev,
137 config,
138 config_parent,
139 root,
140 capabilities: additional_capabilities,
141 assets,
142 test,
143 } = data;
144
145 #[allow(unused_variables)]
146 let running_tests = test;
147
148 let target = std::env::var("TAURI_ENV_TARGET_TRIPLE")
149 .as_deref()
150 .map(Target::from_triple)
151 .unwrap_or_else(|_| Target::current());
152
153 let mut options = AssetOptions::new(config.app.security.pattern.clone())
154 .freeze_prototype(config.app.security.freeze_prototype)
155 .dangerous_disable_asset_csp_modification(
156 config
157 .app
158 .security
159 .dangerous_disable_asset_csp_modification
160 .clone(),
161 );
162 let csp = if dev {
163 config
164 .app
165 .security
166 .dev_csp
167 .as_ref()
168 .or(config.app.security.csp.as_ref())
169 } else {
170 config.app.security.csp.as_ref()
171 };
172 if csp.is_some() {
173 options = options.with_csp();
174 }
175
176 let assets = if let Some(assets) = assets {
177 quote!(#assets)
178 } else if dev && config.build.dev_url.is_some() {
179 let assets = EmbeddedAssets::default();
180 quote!(#assets)
181 } else {
182 let assets = match &config.build.frontend_dist {
183 Some(url) => match url {
184 FrontendDist::Url(_url) => Default::default(),
185 FrontendDist::Directory(path) => {
186 let assets_path = config_parent.join(path);
187 if !assets_path.exists() {
188 panic!(
189 "The `frontendDist` configuration is set to `{path:?}` but this path doesn't exist"
190 )
191 }
192 EmbeddedAssets::new(assets_path, &options, map_core_assets(&options))?
193 }
194 FrontendDist::Files(files) => EmbeddedAssets::new(
195 files
196 .iter()
197 .map(|p| config_parent.join(p))
198 .collect::<Vec<_>>(),
199 &options,
200 map_core_assets(&options),
201 )?,
202 _ => unimplemented!(),
203 },
204 None => Default::default(),
205 };
206 quote!(#assets)
207 };
208
209 let out_dir = ensure_out_dir()?;
210
211 let default_window_icon = {
212 if target == Target::Windows {
213 quote!(#root::image::default_window_icon_from_app_icon_resource())
215 } else {
216 let icon_path = find_icon(
218 &config,
219 &config_parent,
220 |i| i.ends_with(".png"),
221 "icons/icon.png",
222 );
223 let icon = CachedIcon::new(&root, &icon_path)?;
224 quote!(::std::option::Option::Some(#icon))
225 }
226 };
227
228 let app_icon = if target == Target::MacOS && dev {
229 let mut icon_path = find_icon(
230 &config,
231 &config_parent,
232 |i| i.ends_with(".icns"),
233 "icons/icon.png",
234 );
235 if !icon_path.exists() {
236 icon_path = find_icon(
237 &config,
238 &config_parent,
239 |i| i.ends_with(".png"),
240 "icons/icon.png",
241 );
242 }
243
244 let icon = CachedIcon::new_raw(&root, &icon_path)?;
245 quote!(::std::option::Option::Some(#icon.to_vec()))
246 } else {
247 quote!(::std::option::Option::None)
248 };
249
250 let package_name = if let Some(product_name) = &config.product_name {
251 quote!(#product_name.to_string())
252 } else {
253 quote!(env!("CARGO_PKG_NAME").to_string())
254 };
255 let package_version = if let Some(version) = &config.version {
256 semver::Version::from_str(version)?;
257 quote!(#version.to_string())
258 } else {
259 quote!(env!("CARGO_PKG_VERSION").to_string())
260 };
261 let package_info = quote!(
262 #root::PackageInfo {
263 name: #package_name,
264 version: #package_version.parse().unwrap(),
265 authors: env!("CARGO_PKG_AUTHORS"),
266 description: env!("CARGO_PKG_DESCRIPTION"),
267 crate_name: env!("CARGO_PKG_NAME"),
268 }
269 );
270
271 let with_tray_icon_code = if target.is_desktop() {
272 if let Some(tray) = &config.app.tray_icon {
273 let tray_icon_icon_path = config_parent.join(&tray.icon_path);
274 let icon = CachedIcon::new(&root, &tray_icon_icon_path)?;
275 quote!(context.set_tray_icon(::std::option::Option::Some(#icon));)
276 } else {
277 quote!()
278 }
279 } else {
280 quote!()
281 };
282
283 #[cfg(target_os = "macos")]
284 let maybe_embed_plist_block = if target == Target::MacOS && dev && !running_tests {
285 let info_plist_path = config_parent.join("Info.plist");
286 let mut info_plist = if info_plist_path.exists() {
287 plist::Value::from_file(&info_plist_path)
288 .unwrap_or_else(|e| panic!("failed to read plist {}: {}", info_plist_path.display(), e))
289 } else {
290 plist::Value::Dictionary(Default::default())
291 };
292
293 if let Some(plist) = info_plist.as_dictionary_mut() {
294 if let Some(bundle_name) = config
295 .bundle
296 .macos
297 .bundle_name
298 .as_ref()
299 .or(config.product_name.as_ref())
300 {
301 plist.insert("CFBundleName".into(), bundle_name.as_str().into());
302 }
303
304 if let Some(version) = &config.version {
305 let bundle_version = &config.bundle.macos.bundle_version;
306 plist.insert("CFBundleShortVersionString".into(), version.clone().into());
307 plist.insert(
308 "CFBundleVersion".into(),
309 bundle_version
310 .clone()
311 .unwrap_or_else(|| version.clone())
312 .into(),
313 );
314 }
315 }
316
317 let mut plist_contents = std::io::BufWriter::new(Vec::new());
318 info_plist
319 .to_writer_xml(&mut plist_contents)
320 .expect("failed to serialize plist");
321 let plist_contents =
322 String::from_utf8_lossy(&plist_contents.into_inner().unwrap()).into_owned();
323
324 let plist = crate::Cached::try_from(plist_contents)?;
325 quote!({
326 tauri::embed_plist::embed_info_plist!(#plist);
327 })
328 } else {
329 quote!()
330 };
331 #[cfg(not(target_os = "macos"))]
332 let maybe_embed_plist_block = quote!();
333
334 let pattern = match &options.pattern {
335 PatternKind::Brownfield => quote!(#root::Pattern::Brownfield),
336 #[cfg(not(feature = "isolation"))]
337 PatternKind::Isolation { dir: _ } => {
338 quote!(#root::Pattern::Brownfield)
339 }
340 #[cfg(feature = "isolation")]
341 PatternKind::Isolation { dir } => {
342 let dir = config_parent.join(dir);
343 if !dir.exists() {
344 panic!("The isolation application path is set to `{dir:?}` but it does not exist")
345 }
346
347 let mut sets_isolation_hook = false;
348
349 let key = uuid::Uuid::new_v4().to_string();
350 let map_isolation = map_isolation(&options, dir.clone());
351 let assets = EmbeddedAssets::new(dir, &options, |key, path, input, csp_hashes| {
352 if String::from_utf8_lossy(input).contains("__TAURI_ISOLATION_HOOK__") {
355 sets_isolation_hook = true;
356 }
357 map_isolation(key, path, input, csp_hashes)
358 })?;
359
360 if !sets_isolation_hook {
361 panic!(
362 "The isolation application does not contain a file setting the `window.__TAURI_ISOLATION_HOOK__` value."
363 );
364 }
365
366 let schema = options.isolation_schema;
367
368 quote!(#root::Pattern::Isolation {
369 assets: ::std::sync::Arc::new(#assets),
370 schema: #schema.into(),
371 key: #key.into(),
372 crypto_keys: std::boxed::Box::new(::tauri::utils::pattern::isolation::Keys::new().expect("unable to generate cryptographically secure keys for Tauri \"Isolation\" Pattern")),
373 })
374 }
375 };
376
377 let acl_file_path = out_dir.join(ACL_MANIFESTS_FILE_NAME);
378 let acl: BTreeMap<String, Manifest> = if acl_file_path.exists() {
379 let acl_file =
380 std::fs::read_to_string(acl_file_path).expect("failed to read plugin manifest map");
381 serde_json::from_str(&acl_file).expect("failed to parse plugin manifest map")
382 } else {
383 Default::default()
384 };
385
386 let capabilities_file_path = out_dir.join(CAPABILITIES_FILE_NAME);
387 let capabilities_from_files = if capabilities_file_path.exists() {
388 let capabilities_json =
389 std::fs::read_to_string(&capabilities_file_path).expect("failed to read capabilities");
390 serde_json::from_str(&capabilities_json).expect("failed to parse capabilities")
391 } else {
392 Default::default()
393 };
394 let capabilities = get_capabilities(
395 &config,
396 capabilities_from_files,
397 additional_capabilities.as_deref(),
398 )
399 .unwrap();
400
401 let resolved = Resolved::resolve(&acl, capabilities, target).expect("failed to resolve ACL");
402
403 let acl_tokens = map_lit(
404 quote! { ::std::collections::BTreeMap },
405 &acl,
406 str_lit,
407 identity,
408 );
409
410 let runtime_authority = quote!(#root::runtime_authority!(#acl_tokens, #resolved));
411
412 let plugin_global_api_scripts = if config.app.with_global_tauri {
413 if let Some(scripts) = tauri_utils::plugin::read_global_api_scripts(&out_dir) {
414 let scripts = scripts.into_iter().map(|s| quote!(#s));
415 quote!(::std::option::Option::Some(&[#(#scripts),*]))
416 } else {
417 quote!(::std::option::Option::None)
418 }
419 } else {
420 quote!(::std::option::Option::None)
421 };
422
423 let maybe_config_parent_setter = if dev {
424 let config_parent = config_parent.to_string_lossy();
425 quote!({
426 context.with_config_parent(#config_parent);
427 })
428 } else {
429 quote!()
430 };
431
432 let context = quote!({
433 #maybe_embed_plist_block
434
435 #[allow(unused_mut, clippy::let_and_return)]
436 let mut context = #root::Context::new(
437 #config,
438 ::std::boxed::Box::new(assets),
439 #default_window_icon,
440 #app_icon,
441 #package_info,
442 #pattern,
443 #runtime_authority,
444 #plugin_global_api_scripts
445 );
446
447 #with_tray_icon_code
448 #maybe_config_parent_setter
449
450 context
451 });
452
453 let output = quote!({
458 fn inner<R: #root::Runtime, A: #root::Assets<R> + 'static>(assets: A) -> #root::Context<R> {
459 let thread = ::std::thread::Builder::new()
460 .name(String::from("generated tauri context creation"))
461 .stack_size(8 * 1024 * 1024)
462 .spawn(move || #context)
463 .expect("unable to create thread with 8MiB stack");
464
465 match thread.join() {
466 Ok(context) => context,
467 Err(_) => {
468 eprintln!("the generated Tauri `Context` panicked during creation");
469 ::std::process::exit(101);
470 }
471 }
472 }
473 inner(#assets)
474 });
475
476 Ok(output)
477}
478
479fn find_icon(
480 config: &Config,
481 config_parent: &Path,
482 predicate: impl Fn(&&String) -> bool,
483 default: &str,
484) -> PathBuf {
485 let icon_path = config
486 .bundle
487 .icon
488 .iter()
489 .find(predicate)
490 .map(AsRef::as_ref)
491 .unwrap_or(default);
492 config_parent.join(icon_path)
493}