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 ensure_out_dir, AssetOptions, CspHashes, EmbeddedAssets, EmbeddedAssetsResult,
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::acl::{ACL_MANIFESTS_FILE_NAME, CAPABILITIES_FILE_NAME};
22use tauri_utils::{
23 acl::capability::{Capability, CapabilityFile},
24 acl::manifest::Manifest,
25 acl::resolved::Resolved,
26 assets::AssetKey,
27 config::{CapabilityEntry, Config, FrontendDist, PatternKind},
28 html::{inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node, NodeRef},
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: &NodeRef, key: &AssetKey, csp_hashes: &mut CspHashes) {
48 if let Ok(inline_script_elements) = document.select("script:not(empty)") {
49 let mut scripts = Vec::new();
50 for inline_script_el in inline_script_elements {
51 let script = inline_script_el.as_node().text_contents();
52 let mut hasher = Sha256::new();
53 hasher.update(&script);
54 let hash = hasher.finalize();
55 scripts.push(format!(
56 "'sha256-{}'",
57 base64::engine::general_purpose::STANDARD.encode(hash)
58 ));
59 }
60 csp_hashes
61 .inline_scripts
62 .entry(key.clone().into())
63 .or_default()
64 .append(&mut scripts);
65 }
66}
67
68fn map_core_assets(
69 options: &AssetOptions,
70) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> EmbeddedAssetsResult<()> {
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_html(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_html_node(&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<()> {
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_html(String::from_utf8_lossy(input).into_owned());
110
111 tauri_utils::html::inject_codegen_isolation_script(&isolation_html);
113
114 tauri_utils::html::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 = isolation_html.to_string().as_bytes().to_vec()
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 let icon_path = find_icon(
215 &config,
216 &config_parent,
217 |i| i.ends_with(".ico"),
218 "icons/icon.ico",
219 );
220 if icon_path.exists() {
221 let icon = CachedIcon::new(&root, &icon_path)?;
222 quote!(::std::option::Option::Some(#icon))
223 } else {
224 let icon_path = find_icon(
225 &config,
226 &config_parent,
227 |i| i.ends_with(".png"),
228 "icons/icon.png",
229 );
230 let icon = CachedIcon::new(&root, &icon_path)?;
231 quote!(::std::option::Option::Some(#icon))
232 }
233 } else {
234 let icon_path = find_icon(
236 &config,
237 &config_parent,
238 |i| i.ends_with(".png"),
239 "icons/icon.png",
240 );
241 let icon = CachedIcon::new(&root, &icon_path)?;
242 quote!(::std::option::Option::Some(#icon))
243 }
244 };
245
246 let app_icon = if target == Target::MacOS && dev {
247 let mut icon_path = find_icon(
248 &config,
249 &config_parent,
250 |i| i.ends_with(".icns"),
251 "icons/icon.png",
252 );
253 if !icon_path.exists() {
254 icon_path = find_icon(
255 &config,
256 &config_parent,
257 |i| i.ends_with(".png"),
258 "icons/icon.png",
259 );
260 }
261
262 let icon = CachedIcon::new_raw(&root, &icon_path)?;
263 quote!(::std::option::Option::Some(#icon.to_vec()))
264 } else {
265 quote!(::std::option::Option::None)
266 };
267
268 let package_name = if let Some(product_name) = &config.product_name {
269 quote!(#product_name.to_string())
270 } else {
271 quote!(env!("CARGO_PKG_NAME").to_string())
272 };
273 let package_version = if let Some(version) = &config.version {
274 semver::Version::from_str(version)?;
275 quote!(#version.to_string())
276 } else {
277 quote!(env!("CARGO_PKG_VERSION").to_string())
278 };
279 let package_info = quote!(
280 #root::PackageInfo {
281 name: #package_name,
282 version: #package_version.parse().unwrap(),
283 authors: env!("CARGO_PKG_AUTHORS"),
284 description: env!("CARGO_PKG_DESCRIPTION"),
285 crate_name: env!("CARGO_PKG_NAME"),
286 }
287 );
288
289 let with_tray_icon_code = if target.is_desktop() {
290 if let Some(tray) = &config.app.tray_icon {
291 let tray_icon_icon_path = config_parent.join(&tray.icon_path);
292 let icon = CachedIcon::new(&root, &tray_icon_icon_path)?;
293 quote!(context.set_tray_icon(::std::option::Option::Some(#icon));)
294 } else {
295 quote!()
296 }
297 } else {
298 quote!()
299 };
300
301 #[cfg(target_os = "macos")]
302 let maybe_embed_plist_block = if target == Target::MacOS && dev && !running_tests {
303 let info_plist_path = config_parent.join("Info.plist");
304 let mut info_plist = if info_plist_path.exists() {
305 plist::Value::from_file(&info_plist_path)
306 .unwrap_or_else(|e| panic!("failed to read plist {}: {}", info_plist_path.display(), e))
307 } else {
308 plist::Value::Dictionary(Default::default())
309 };
310
311 if let Some(plist) = info_plist.as_dictionary_mut() {
312 if let Some(product_name) = &config.product_name {
313 plist.insert("CFBundleName".into(), product_name.clone().into());
314 }
315 if let Some(version) = &config.version {
316 plist.insert("CFBundleShortVersionString".into(), version.clone().into());
317 plist.insert("CFBundleVersion".into(), version.clone().into());
318 }
319 }
320
321 let mut plist_contents = std::io::BufWriter::new(Vec::new());
322 info_plist
323 .to_writer_xml(&mut plist_contents)
324 .expect("failed to serialize plist");
325 let plist_contents =
326 String::from_utf8_lossy(&plist_contents.into_inner().unwrap()).into_owned();
327
328 let plist = crate::Cached::try_from(plist_contents)?;
329 quote!({
330 tauri::embed_plist::embed_info_plist!(#plist);
331 })
332 } else {
333 quote!()
334 };
335 #[cfg(not(target_os = "macos"))]
336 let maybe_embed_plist_block = quote!();
337
338 let pattern = match &options.pattern {
339 PatternKind::Brownfield => quote!(#root::Pattern::Brownfield),
340 #[cfg(not(feature = "isolation"))]
341 PatternKind::Isolation { dir: _ } => {
342 quote!(#root::Pattern::Brownfield)
343 }
344 #[cfg(feature = "isolation")]
345 PatternKind::Isolation { dir } => {
346 let dir = config_parent.join(dir);
347 if !dir.exists() {
348 panic!("The isolation application path is set to `{dir:?}` but it does not exist")
349 }
350
351 let mut sets_isolation_hook = false;
352
353 let key = uuid::Uuid::new_v4().to_string();
354 let map_isolation = map_isolation(&options, dir.clone());
355 let assets = EmbeddedAssets::new(dir, &options, |key, path, input, csp_hashes| {
356 if String::from_utf8_lossy(input).contains("__TAURI_ISOLATION_HOOK__") {
359 sets_isolation_hook = true;
360 }
361 map_isolation(key, path, input, csp_hashes)
362 })?;
363
364 if !sets_isolation_hook {
365 panic!("The isolation application does not contain a file setting the `window.__TAURI_ISOLATION_HOOK__` value.");
366 }
367
368 let schema = options.isolation_schema;
369
370 quote!(#root::Pattern::Isolation {
371 assets: ::std::sync::Arc::new(#assets),
372 schema: #schema.into(),
373 key: #key.into(),
374 crypto_keys: std::boxed::Box::new(::tauri::utils::pattern::isolation::Keys::new().expect("unable to generate cryptographically secure keys for Tauri \"Isolation\" Pattern")),
375 })
376 }
377 };
378
379 let acl_file_path = out_dir.join(ACL_MANIFESTS_FILE_NAME);
380 let acl: BTreeMap<String, Manifest> = if acl_file_path.exists() {
381 let acl_file =
382 std::fs::read_to_string(acl_file_path).expect("failed to read plugin manifest map");
383 serde_json::from_str(&acl_file).expect("failed to parse plugin manifest map")
384 } else {
385 Default::default()
386 };
387
388 let capabilities_file_path = out_dir.join(CAPABILITIES_FILE_NAME);
389 let mut capabilities_from_files: BTreeMap<String, Capability> = if capabilities_file_path.exists()
390 {
391 let capabilities_file =
392 std::fs::read_to_string(capabilities_file_path).expect("failed to read capabilities");
393 serde_json::from_str(&capabilities_file).expect("failed to parse capabilities")
394 } else {
395 Default::default()
396 };
397
398 let mut capabilities = if config.app.security.capabilities.is_empty() {
399 capabilities_from_files
400 } else {
401 let mut capabilities = BTreeMap::new();
402 for capability_entry in &config.app.security.capabilities {
403 match capability_entry {
404 CapabilityEntry::Inlined(capability) => {
405 capabilities.insert(capability.identifier.clone(), capability.clone());
406 }
407 CapabilityEntry::Reference(id) => {
408 let capability = capabilities_from_files
409 .remove(id)
410 .unwrap_or_else(|| panic!("capability with identifier {id} not found"));
411 capabilities.insert(id.clone(), capability);
412 }
413 }
414 }
415 capabilities
416 };
417
418 let acl_tokens = map_lit(
419 quote! { ::std::collections::BTreeMap },
420 &acl,
421 str_lit,
422 identity,
423 );
424
425 if let Some(paths) = additional_capabilities {
426 for path in paths {
427 let capability = CapabilityFile::load(&path)
428 .unwrap_or_else(|e| panic!("failed to read capability {}: {e}", path.display()));
429 match capability {
430 CapabilityFile::Capability(c) => {
431 capabilities.insert(c.identifier.clone(), c);
432 }
433 CapabilityFile::List(capabilities_list)
434 | CapabilityFile::NamedList {
435 capabilities: capabilities_list,
436 } => {
437 capabilities.extend(
438 capabilities_list
439 .into_iter()
440 .map(|c| (c.identifier.clone(), c)),
441 );
442 }
443 }
444 }
445 }
446
447 let resolved = Resolved::resolve(&acl, capabilities, target).expect("failed to resolve ACL");
448 let runtime_authority = quote!(#root::ipc::RuntimeAuthority::new(#acl_tokens, #resolved));
449
450 let plugin_global_api_scripts = if config.app.with_global_tauri {
451 if let Some(scripts) = tauri_utils::plugin::read_global_api_scripts(&out_dir) {
452 let scripts = scripts.into_iter().map(|s| quote!(#s));
453 quote!(::std::option::Option::Some(&[#(#scripts),*]))
454 } else {
455 quote!(::std::option::Option::None)
456 }
457 } else {
458 quote!(::std::option::Option::None)
459 };
460
461 let maybe_config_parent_setter = if dev {
462 let config_parent = config_parent.to_string_lossy();
463 quote!({
464 context.with_config_parent(#config_parent);
465 })
466 } else {
467 quote!()
468 };
469
470 let context = quote!({
471 #maybe_embed_plist_block
472
473 #[allow(unused_mut, clippy::let_and_return)]
474 let mut context = #root::Context::new(
475 #config,
476 ::std::boxed::Box::new(#assets),
477 #default_window_icon,
478 #app_icon,
479 #package_info,
480 #pattern,
481 #runtime_authority,
482 #plugin_global_api_scripts
483 );
484
485 #with_tray_icon_code
486 #maybe_config_parent_setter
487
488 context
489 });
490
491 Ok(quote!({
492 let thread = ::std::thread::Builder::new()
493 .name(String::from("generated tauri context creation"))
494 .stack_size(8 * 1024 * 1024)
495 .spawn(|| #context)
496 .expect("unable to create thread with 8MiB stack");
497
498 match thread.join() {
499 Ok(context) => context,
500 Err(_) => {
501 eprintln!("the generated Tauri `Context` panicked during creation");
502 ::std::process::exit(101);
503 }
504 }
505 }))
506}
507
508fn find_icon(
509 config: &Config,
510 config_parent: &Path,
511 predicate: impl Fn(&&String) -> bool,
512 default: &str,
513) -> PathBuf {
514 let icon_path = config
515 .bundle
516 .icon
517 .iter()
518 .find(predicate)
519 .map(AsRef::as_ref)
520 .unwrap_or(default);
521 config_parent.join(icon_path)
522}