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(&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(bundle_name) = config
313 .bundle
314 .macos
315 .bundle_name
316 .as_ref()
317 .or(config.product_name.as_ref())
318 {
319 plist.insert("CFBundleName".into(), bundle_name.as_str().into());
320 }
321
322 if let Some(version) = &config.version {
323 let bundle_version = &config.bundle.macos.bundle_version;
324 plist.insert("CFBundleShortVersionString".into(), version.clone().into());
325 plist.insert(
326 "CFBundleVersion".into(),
327 bundle_version
328 .clone()
329 .unwrap_or_else(|| version.clone())
330 .into(),
331 );
332 }
333 }
334
335 let mut plist_contents = std::io::BufWriter::new(Vec::new());
336 info_plist
337 .to_writer_xml(&mut plist_contents)
338 .expect("failed to serialize plist");
339 let plist_contents =
340 String::from_utf8_lossy(&plist_contents.into_inner().unwrap()).into_owned();
341
342 let plist = crate::Cached::try_from(plist_contents)?;
343 quote!({
344 tauri::embed_plist::embed_info_plist!(#plist);
345 })
346 } else {
347 quote!()
348 };
349 #[cfg(not(target_os = "macos"))]
350 let maybe_embed_plist_block = quote!();
351
352 let pattern = match &options.pattern {
353 PatternKind::Brownfield => quote!(#root::Pattern::Brownfield),
354 #[cfg(not(feature = "isolation"))]
355 PatternKind::Isolation { dir: _ } => {
356 quote!(#root::Pattern::Brownfield)
357 }
358 #[cfg(feature = "isolation")]
359 PatternKind::Isolation { dir } => {
360 let dir = config_parent.join(dir);
361 if !dir.exists() {
362 panic!("The isolation application path is set to `{dir:?}` but it does not exist")
363 }
364
365 let mut sets_isolation_hook = false;
366
367 let key = uuid::Uuid::new_v4().to_string();
368 let map_isolation = map_isolation(&options, dir.clone());
369 let assets = EmbeddedAssets::new(dir, &options, |key, path, input, csp_hashes| {
370 if String::from_utf8_lossy(input).contains("__TAURI_ISOLATION_HOOK__") {
373 sets_isolation_hook = true;
374 }
375 map_isolation(key, path, input, csp_hashes)
376 })?;
377
378 if !sets_isolation_hook {
379 panic!("The isolation application does not contain a file setting the `window.__TAURI_ISOLATION_HOOK__` value.");
380 }
381
382 let schema = options.isolation_schema;
383
384 quote!(#root::Pattern::Isolation {
385 assets: ::std::sync::Arc::new(#assets),
386 schema: #schema.into(),
387 key: #key.into(),
388 crypto_keys: std::boxed::Box::new(::tauri::utils::pattern::isolation::Keys::new().expect("unable to generate cryptographically secure keys for Tauri \"Isolation\" Pattern")),
389 })
390 }
391 };
392
393 let acl_file_path = out_dir.join(ACL_MANIFESTS_FILE_NAME);
394 let acl: BTreeMap<String, Manifest> = if acl_file_path.exists() {
395 let acl_file =
396 std::fs::read_to_string(acl_file_path).expect("failed to read plugin manifest map");
397 serde_json::from_str(&acl_file).expect("failed to parse plugin manifest map")
398 } else {
399 Default::default()
400 };
401
402 let capabilities_file_path = out_dir.join(CAPABILITIES_FILE_NAME);
403 let capabilities_from_files = if capabilities_file_path.exists() {
404 let capabilities_json =
405 std::fs::read_to_string(&capabilities_file_path).expect("failed to read capabilities");
406 serde_json::from_str(&capabilities_json).expect("failed to parse capabilities")
407 } else {
408 Default::default()
409 };
410 let capabilities = get_capabilities(
411 &config,
412 capabilities_from_files,
413 additional_capabilities.as_deref(),
414 )
415 .unwrap();
416
417 let resolved = Resolved::resolve(&acl, capabilities, target).expect("failed to resolve ACL");
418
419 let acl_tokens = map_lit(
420 quote! { ::std::collections::BTreeMap },
421 &acl,
422 str_lit,
423 identity,
424 );
425
426 let runtime_authority = quote!(#root::runtime_authority!(#acl_tokens, #resolved));
427
428 let plugin_global_api_scripts = if config.app.with_global_tauri {
429 if let Some(scripts) = tauri_utils::plugin::read_global_api_scripts(&out_dir) {
430 let scripts = scripts.into_iter().map(|s| quote!(#s));
431 quote!(::std::option::Option::Some(&[#(#scripts),*]))
432 } else {
433 quote!(::std::option::Option::None)
434 }
435 } else {
436 quote!(::std::option::Option::None)
437 };
438
439 let maybe_config_parent_setter = if dev {
440 let config_parent = config_parent.to_string_lossy();
441 quote!({
442 context.with_config_parent(#config_parent);
443 })
444 } else {
445 quote!()
446 };
447
448 let context = quote!({
449 #maybe_embed_plist_block
450
451 #[allow(unused_mut, clippy::let_and_return)]
452 let mut context = #root::Context::new(
453 #config,
454 ::std::boxed::Box::new(#assets),
455 #default_window_icon,
456 #app_icon,
457 #package_info,
458 #pattern,
459 #runtime_authority,
460 #plugin_global_api_scripts
461 );
462
463 #with_tray_icon_code
464 #maybe_config_parent_setter
465
466 context
467 });
468
469 Ok(quote!({
470 let thread = ::std::thread::Builder::new()
471 .name(String::from("generated tauri context creation"))
472 .stack_size(8 * 1024 * 1024)
473 .spawn(|| #context)
474 .expect("unable to create thread with 8MiB stack");
475
476 match thread.join() {
477 Ok(context) => context,
478 Err(_) => {
479 eprintln!("the generated Tauri `Context` panicked during creation");
480 ::std::process::exit(101);
481 }
482 }
483 }))
484}
485
486fn find_icon(
487 config: &Config,
488 config_parent: &Path,
489 predicate: impl Fn(&&String) -> bool,
490 default: &str,
491) -> PathBuf {
492 let icon_path = config
493 .bundle
494 .icon
495 .iter()
496 .find(predicate)
497 .map(AsRef::as_ref)
498 .unwrap_or(default);
499 config_parent.join(icon_path)
500}