context.rs 19 KB

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