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