context.rs 19 KB

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