context.rs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use std::path::{Path, PathBuf};
  5. use std::{ffi::OsStr, str::FromStr};
  6. use base64::Engine;
  7. use proc_macro2::TokenStream;
  8. use quote::quote;
  9. use sha2::{Digest, Sha256};
  10. use tauri_utils::assets::AssetKey;
  11. use tauri_utils::config::{AppUrl, Config, PatternKind, WindowUrl};
  12. use tauri_utils::html::{
  13. inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node,
  14. };
  15. use crate::embedded_assets::{AssetOptions, CspHashes, EmbeddedAssets, EmbeddedAssetsError};
  16. /// Necessary data needed by [`context_codegen`] to generate code for a Tauri application context.
  17. pub struct ContextData {
  18. pub dev: bool,
  19. pub config: Config,
  20. pub config_parent: PathBuf,
  21. pub root: TokenStream,
  22. }
  23. fn map_core_assets(
  24. options: &AssetOptions,
  25. target: Target,
  26. ) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> Result<(), EmbeddedAssetsError> {
  27. #[cfg(feature = "isolation")]
  28. let pattern = tauri_utils::html::PatternObject::from(&options.pattern);
  29. let csp = options.csp;
  30. let dangerous_disable_asset_csp_modification =
  31. options.dangerous_disable_asset_csp_modification.clone();
  32. move |key, path, input, csp_hashes| {
  33. if path.extension() == Some(OsStr::new("html")) {
  34. #[allow(clippy::collapsible_if)]
  35. if csp {
  36. let mut document = parse_html(String::from_utf8_lossy(input).into_owned());
  37. if target == Target::Linux {
  38. ::tauri_utils::html::inject_csp_token(&mut document);
  39. }
  40. inject_nonce_token(&mut document, &dangerous_disable_asset_csp_modification);
  41. if dangerous_disable_asset_csp_modification.can_modify("script-src") {
  42. if let Ok(inline_script_elements) = document.select("script:not(empty)") {
  43. let mut scripts = Vec::new();
  44. for inline_script_el in inline_script_elements {
  45. let script = inline_script_el.as_node().text_contents();
  46. let mut hasher = Sha256::new();
  47. hasher.update(&script);
  48. let hash = hasher.finalize();
  49. scripts.push(format!(
  50. "'sha256-{}'",
  51. base64::engine::general_purpose::STANDARD.encode(hash)
  52. ));
  53. }
  54. csp_hashes
  55. .inline_scripts
  56. .entry(key.clone().into())
  57. .or_default()
  58. .append(&mut scripts);
  59. }
  60. }
  61. #[cfg(feature = "isolation")]
  62. if dangerous_disable_asset_csp_modification.can_modify("style-src") {
  63. if let tauri_utils::html::PatternObject::Isolation { .. } = &pattern {
  64. // create the csp for the isolation iframe styling now, to make the runtime less complex
  65. let mut hasher = Sha256::new();
  66. hasher.update(tauri_utils::pattern::isolation::IFRAME_STYLE);
  67. let hash = hasher.finalize();
  68. csp_hashes.styles.push(format!(
  69. "'sha256-{}'",
  70. base64::engine::general_purpose::STANDARD.encode(hash)
  71. ));
  72. }
  73. }
  74. *input = serialize_html_node(&document);
  75. }
  76. }
  77. Ok(())
  78. }
  79. }
  80. #[cfg(feature = "isolation")]
  81. fn map_isolation(
  82. _options: &AssetOptions,
  83. dir: PathBuf,
  84. ) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> Result<(), EmbeddedAssetsError> {
  85. move |_key, path, input, _csp_hashes| {
  86. if path.extension() == Some(OsStr::new("html")) {
  87. let mut isolation_html =
  88. tauri_utils::html::parse(String::from_utf8_lossy(input).into_owned());
  89. // this is appended, so no need to reverse order it
  90. tauri_utils::html::inject_codegen_isolation_script(&mut isolation_html);
  91. // temporary workaround for windows not loading assets
  92. tauri_utils::html::inline_isolation(&mut isolation_html, &dir);
  93. *input = isolation_html.to_string().as_bytes().to_vec()
  94. }
  95. Ok(())
  96. }
  97. }
  98. #[derive(PartialEq, Eq, Clone, Copy)]
  99. enum Target {
  100. Linux,
  101. Windows,
  102. Darwin,
  103. Android,
  104. // iOS.
  105. Ios,
  106. }
  107. impl Target {
  108. fn is_mobile(&self) -> bool {
  109. matches!(self, Target::Android | Target::Ios)
  110. }
  111. fn is_desktop(&self) -> bool {
  112. !self.is_mobile()
  113. }
  114. }
  115. /// Build a `tauri::Context` for including in application code.
  116. pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsError> {
  117. let ContextData {
  118. dev,
  119. config,
  120. config_parent,
  121. root,
  122. } = data;
  123. let target =
  124. if let Ok(target) = std::env::var("TARGET").or_else(|_| std::env::var("TAURI_TARGET_TRIPLE")) {
  125. if target.contains("unknown-linux") {
  126. Target::Linux
  127. } else if target.contains("pc-windows") {
  128. Target::Windows
  129. } else if target.contains("apple-darwin") {
  130. Target::Darwin
  131. } else if target.contains("android") {
  132. Target::Android
  133. } else if target.contains("apple-ios") {
  134. Target::Ios
  135. } else {
  136. panic!("unknown codegen target {target}");
  137. }
  138. } else if cfg!(target_os = "linux") {
  139. Target::Linux
  140. } else if cfg!(windows) {
  141. Target::Windows
  142. } else if cfg!(target_os = "macos") {
  143. Target::Darwin
  144. } else if cfg!(target_os = "android") {
  145. Target::Android
  146. } else if cfg!(target_os = "ios") {
  147. Target::Ios
  148. } else {
  149. panic!("unknown codegen target")
  150. };
  151. let mut options = AssetOptions::new(config.tauri.pattern.clone())
  152. .freeze_prototype(config.tauri.security.freeze_prototype)
  153. .dangerous_disable_asset_csp_modification(
  154. config
  155. .tauri
  156. .security
  157. .dangerous_disable_asset_csp_modification
  158. .clone(),
  159. );
  160. let csp = if dev {
  161. config
  162. .tauri
  163. .security
  164. .dev_csp
  165. .clone()
  166. .or_else(|| config.tauri.security.csp.clone())
  167. } else {
  168. config.tauri.security.csp.clone()
  169. };
  170. if csp.is_some() {
  171. options = options.with_csp();
  172. }
  173. let app_url = if dev {
  174. &config.build.dev_path
  175. } else {
  176. &config.build.dist_dir
  177. };
  178. let assets = match app_url {
  179. AppUrl::Url(url) => match url {
  180. WindowUrl::External(_) => Default::default(),
  181. WindowUrl::App(path) => {
  182. if path.components().count() == 0 {
  183. panic!(
  184. "The `{}` configuration cannot be empty",
  185. if dev { "devPath" } else { "distDir" }
  186. )
  187. }
  188. let assets_path = config_parent.join(path);
  189. if !assets_path.exists() {
  190. panic!(
  191. "The `{}` configuration is set to `{:?}` but this path doesn't exist",
  192. if dev { "devPath" } else { "distDir" },
  193. path
  194. )
  195. }
  196. EmbeddedAssets::new(assets_path, &options, map_core_assets(&options, target))?
  197. }
  198. _ => unimplemented!(),
  199. },
  200. AppUrl::Files(files) => EmbeddedAssets::new(
  201. files
  202. .iter()
  203. .map(|p| config_parent.join(p))
  204. .collect::<Vec<_>>(),
  205. &options,
  206. map_core_assets(&options, target),
  207. )?,
  208. _ => unimplemented!(),
  209. };
  210. let out_dir = {
  211. let out_dir = std::env::var("OUT_DIR")
  212. .map_err(|_| EmbeddedAssetsError::OutDir)
  213. .map(PathBuf::from)
  214. .and_then(|p| p.canonicalize().map_err(|_| EmbeddedAssetsError::OutDir))?;
  215. // make sure that our output directory is created
  216. std::fs::create_dir_all(&out_dir).map_err(|_| EmbeddedAssetsError::OutDir)?;
  217. out_dir
  218. };
  219. let default_window_icon = {
  220. if target == Target::Windows {
  221. // handle default window icons for Windows targets
  222. let icon_path = find_icon(
  223. &config,
  224. &config_parent,
  225. |i| i.ends_with(".ico"),
  226. "icons/icon.ico",
  227. );
  228. if icon_path.exists() {
  229. ico_icon(&root, &out_dir, icon_path).map(|i| quote!(::std::option::Option::Some(#i)))?
  230. } else {
  231. let icon_path = find_icon(
  232. &config,
  233. &config_parent,
  234. |i| i.ends_with(".png"),
  235. "icons/icon.png",
  236. );
  237. png_icon(&root, &out_dir, icon_path).map(|i| quote!(::std::option::Option::Some(#i)))?
  238. }
  239. } else if target == Target::Linux {
  240. // handle default window icons for Linux targets
  241. let icon_path = find_icon(
  242. &config,
  243. &config_parent,
  244. |i| i.ends_with(".png"),
  245. "icons/icon.png",
  246. );
  247. png_icon(&root, &out_dir, icon_path).map(|i| quote!(::std::option::Option::Some(#i)))?
  248. } else {
  249. quote!(::std::option::Option::None)
  250. }
  251. };
  252. let app_icon = if target == Target::Darwin && dev {
  253. let mut icon_path = find_icon(
  254. &config,
  255. &config_parent,
  256. |i| i.ends_with(".icns"),
  257. "icons/icon.png",
  258. );
  259. if !icon_path.exists() {
  260. icon_path = find_icon(
  261. &config,
  262. &config_parent,
  263. |i| i.ends_with(".png"),
  264. "icons/icon.png",
  265. );
  266. }
  267. raw_icon(&out_dir, icon_path)?
  268. } else {
  269. quote!(::std::option::Option::None)
  270. };
  271. let package_name = if let Some(product_name) = &config.package.product_name {
  272. quote!(#product_name.to_string())
  273. } else {
  274. quote!(env!("CARGO_PKG_NAME").to_string())
  275. };
  276. let package_version = if let Some(version) = &config.package.version {
  277. semver::Version::from_str(version)?;
  278. quote!(#version.to_string())
  279. } else {
  280. quote!(env!("CARGO_PKG_VERSION").to_string())
  281. };
  282. let package_info = quote!(
  283. #root::PackageInfo {
  284. name: #package_name,
  285. version: #package_version.parse().unwrap(),
  286. authors: env!("CARGO_PKG_AUTHORS"),
  287. description: env!("CARGO_PKG_DESCRIPTION"),
  288. crate_name: env!("CARGO_PKG_NAME"),
  289. }
  290. );
  291. let with_system_tray_icon_code = if target.is_desktop() {
  292. if let Some(tray) = &config.tauri.system_tray {
  293. let system_tray_icon_path = config_parent.join(&tray.icon_path);
  294. let ext = system_tray_icon_path.extension();
  295. if ext.map_or(false, |e| e == "ico") {
  296. ico_icon(&root, &out_dir, system_tray_icon_path)
  297. .map(|i| quote!(context.set_system_tray_icon(#i);))?
  298. } else if ext.map_or(false, |e| e == "png") {
  299. png_icon(&root, &out_dir, system_tray_icon_path)
  300. .map(|i| quote!(context.set_system_tray_icon(#i);))?
  301. } else {
  302. quote!(compile_error!(
  303. "The tray icon extension must be either `.ico` or `.png`."
  304. ))
  305. }
  306. } else {
  307. quote!()
  308. }
  309. } else {
  310. quote!()
  311. };
  312. #[cfg(target_os = "macos")]
  313. let info_plist = if target == Target::Darwin && dev {
  314. let info_plist_path = config_parent.join("Info.plist");
  315. let mut info_plist = if info_plist_path.exists() {
  316. plist::Value::from_file(&info_plist_path)
  317. .unwrap_or_else(|e| panic!("failed to read plist {}: {}", info_plist_path.display(), e))
  318. } else {
  319. plist::Value::Dictionary(Default::default())
  320. };
  321. if let Some(plist) = info_plist.as_dictionary_mut() {
  322. if let Some(product_name) = &config.package.product_name {
  323. plist.insert("CFBundleName".into(), product_name.clone().into());
  324. }
  325. if let Some(version) = &config.package.version {
  326. plist.insert("CFBundleShortVersionString".into(), version.clone().into());
  327. }
  328. let format =
  329. time::format_description::parse("[year][month][day].[hour][minute][second]").unwrap();
  330. if let Ok(build_number) = time::OffsetDateTime::now_utc().format(&format) {
  331. plist.insert("CFBundleVersion".into(), build_number.into());
  332. }
  333. }
  334. let out_path = out_dir.join("Info.plist");
  335. info_plist
  336. .to_file_xml(&out_path)
  337. .expect("failed to write Info.plist");
  338. let info_plist_path = out_path.display().to_string();
  339. quote!({
  340. tauri::embed_plist::embed_info_plist!(#info_plist_path);
  341. })
  342. } else {
  343. quote!(())
  344. };
  345. #[cfg(not(target_os = "macos"))]
  346. let info_plist = quote!(());
  347. let pattern = match &options.pattern {
  348. PatternKind::Brownfield => quote!(#root::Pattern::Brownfield(std::marker::PhantomData)),
  349. #[cfg(not(feature = "isolation"))]
  350. PatternKind::Isolation { dir: _ } => {
  351. quote!(#root::Pattern::Brownfield(std::marker::PhantomData))
  352. }
  353. #[cfg(feature = "isolation")]
  354. PatternKind::Isolation { dir } => {
  355. let dir = config_parent.join(dir);
  356. if !dir.exists() {
  357. panic!("The isolation application path is set to `{dir:?}` but it does not exist")
  358. }
  359. let mut sets_isolation_hook = false;
  360. let key = uuid::Uuid::new_v4().to_string();
  361. let map_isolation = map_isolation(&options, dir.clone());
  362. let assets = EmbeddedAssets::new(dir, &options, |key, path, input, csp_hashes| {
  363. // we check if `__TAURI_ISOLATION_HOOK__` exists in the isolation code
  364. // before modifying the files since we inject our own `__TAURI_ISOLATION_HOOK__` reference in HTML files
  365. if String::from_utf8_lossy(input).contains("__TAURI_ISOLATION_HOOK__") {
  366. sets_isolation_hook = true;
  367. }
  368. map_isolation(key, path, input, csp_hashes)
  369. })?;
  370. if !sets_isolation_hook {
  371. panic!("The isolation application does not contain a file setting the `window.__TAURI_ISOLATION_HOOK__` value.");
  372. }
  373. let schema = options.isolation_schema;
  374. quote!(#root::Pattern::Isolation {
  375. assets: ::std::sync::Arc::new(#assets),
  376. schema: #schema.into(),
  377. key: #key.into(),
  378. crypto_keys: std::boxed::Box::new(::tauri::utils::pattern::isolation::Keys::new().expect("unable to generate cryptographically secure keys for Tauri \"Isolation\" Pattern")),
  379. })
  380. }
  381. };
  382. Ok(quote!({
  383. #[allow(unused_mut, clippy::let_and_return)]
  384. let mut context = #root::Context::new(
  385. #config,
  386. ::std::sync::Arc::new(#assets),
  387. #default_window_icon,
  388. #app_icon,
  389. #package_info,
  390. #info_plist,
  391. #pattern,
  392. );
  393. #with_system_tray_icon_code
  394. context
  395. }))
  396. }
  397. fn ico_icon<P: AsRef<Path>>(
  398. root: &TokenStream,
  399. out_dir: &Path,
  400. path: P,
  401. ) -> Result<TokenStream, EmbeddedAssetsError> {
  402. let path = path.as_ref();
  403. let bytes = std::fs::read(path)
  404. .unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e))
  405. .to_vec();
  406. let icon_dir = ico::IconDir::read(std::io::Cursor::new(bytes))
  407. .unwrap_or_else(|e| panic!("failed to parse icon {}: {}", path.display(), e));
  408. let entry = &icon_dir.entries()[0];
  409. let rgba = entry
  410. .decode()
  411. .unwrap_or_else(|e| panic!("failed to decode icon {}: {}", path.display(), e))
  412. .rgba_data()
  413. .to_vec();
  414. let width = entry.width();
  415. let height = entry.height();
  416. let out_path = out_dir.join(path.file_name().unwrap());
  417. write_if_changed(&out_path, &rgba).map_err(|error| EmbeddedAssetsError::AssetWrite {
  418. path: path.to_owned(),
  419. error,
  420. })?;
  421. let out_path = out_path.display().to_string();
  422. let icon = quote!(#root::Icon::Rgba { rgba: include_bytes!(#out_path).to_vec(), width: #width, height: #height });
  423. Ok(icon)
  424. }
  425. fn raw_icon<P: AsRef<Path>>(out_dir: &Path, path: P) -> Result<TokenStream, EmbeddedAssetsError> {
  426. let path = path.as_ref();
  427. let bytes = std::fs::read(path)
  428. .unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e))
  429. .to_vec();
  430. let out_path = out_dir.join(path.file_name().unwrap());
  431. write_if_changed(&out_path, &bytes).map_err(|error| EmbeddedAssetsError::AssetWrite {
  432. path: path.to_owned(),
  433. error,
  434. })?;
  435. let out_path = out_path.display().to_string();
  436. let icon = quote!(::std::option::Option::Some(
  437. include_bytes!(#out_path).to_vec()
  438. ));
  439. Ok(icon)
  440. }
  441. fn png_icon<P: AsRef<Path>>(
  442. root: &TokenStream,
  443. out_dir: &Path,
  444. path: P,
  445. ) -> Result<TokenStream, EmbeddedAssetsError> {
  446. let path = path.as_ref();
  447. let bytes = std::fs::read(path)
  448. .unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e))
  449. .to_vec();
  450. let decoder = png::Decoder::new(std::io::Cursor::new(bytes));
  451. let mut reader = decoder
  452. .read_info()
  453. .unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e));
  454. let mut buffer: Vec<u8> = Vec::new();
  455. while let Ok(Some(row)) = reader.next_row() {
  456. buffer.extend(row.data());
  457. }
  458. let width = reader.info().width;
  459. let height = reader.info().height;
  460. let out_path = out_dir.join(path.file_name().unwrap());
  461. write_if_changed(&out_path, &buffer).map_err(|error| EmbeddedAssetsError::AssetWrite {
  462. path: path.to_owned(),
  463. error,
  464. })?;
  465. let out_path = out_path.display().to_string();
  466. let icon = quote!(#root::Icon::Rgba { rgba: include_bytes!(#out_path).to_vec(), width: #width, height: #height });
  467. Ok(icon)
  468. }
  469. fn write_if_changed(out_path: &Path, data: &[u8]) -> std::io::Result<()> {
  470. use std::fs::File;
  471. use std::io::Write;
  472. if let Ok(curr) = std::fs::read(out_path) {
  473. if curr == data {
  474. return Ok(());
  475. }
  476. }
  477. let mut out_file = File::create(out_path)?;
  478. out_file.write_all(data)
  479. }
  480. fn find_icon<F: Fn(&&String) -> bool>(
  481. config: &Config,
  482. config_parent: &Path,
  483. predicate: F,
  484. default: &str,
  485. ) -> PathBuf {
  486. let icon_path = config
  487. .tauri
  488. .bundle
  489. .icon
  490. .iter()
  491. .find(|i| predicate(i))
  492. .cloned()
  493. .unwrap_or_else(|| default.to_string());
  494. config_parent.join(icon_path)
  495. }