context.rs 19 KB

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