context.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  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::ffi::OsStr;
  5. use std::path::{Path, PathBuf};
  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, NodeRef};
  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 load_csp(document: &mut NodeRef, key: &AssetKey, csp_hashes: &mut CspHashes) {
  23. #[cfg(target_os = "linux")]
  24. ::tauri_utils::html::inject_csp_token(document);
  25. inject_nonce_token(document);
  26. if let Ok(inline_script_elements) = document.select("script:not(empty)") {
  27. let mut scripts = Vec::new();
  28. for inline_script_el in inline_script_elements {
  29. let script = inline_script_el.as_node().text_contents();
  30. let mut hasher = Sha256::new();
  31. hasher.update(&script);
  32. let hash = hasher.finalize();
  33. scripts.push(format!("'sha256-{}'", base64::encode(&hash)));
  34. }
  35. csp_hashes
  36. .inline_scripts
  37. .entry(key.clone().into())
  38. .or_default()
  39. .append(&mut scripts);
  40. }
  41. }
  42. fn map_core_assets(
  43. options: &AssetOptions,
  44. ) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> Result<(), EmbeddedAssetsError> {
  45. #[cfg(any(feature = "isolation", feature = "__isolation-docs"))]
  46. let pattern = tauri_utils::html::PatternObject::from(&options.pattern);
  47. let csp = options.csp;
  48. move |key, path, input, csp_hashes| {
  49. if path.extension() == Some(OsStr::new("html")) {
  50. let mut document = parse_html(String::from_utf8_lossy(input).into_owned());
  51. if csp {
  52. load_csp(&mut document, key, csp_hashes);
  53. #[cfg(any(feature = "isolation", feature = "__isolation-docs"))]
  54. if let tauri_utils::html::PatternObject::Isolation { .. } = &pattern {
  55. // create the csp for the isolation iframe styling now, to make the runtime less complex
  56. let mut hasher = Sha256::new();
  57. hasher.update(tauri_utils::pattern::isolation::IFRAME_STYLE);
  58. let hash = hasher.finalize();
  59. csp_hashes
  60. .styles
  61. .push(format!("'sha256-{}'", base64::encode(&hash)));
  62. }
  63. }
  64. *input = document.to_string().as_bytes().to_vec();
  65. }
  66. Ok(())
  67. }
  68. }
  69. #[cfg(any(feature = "isolation", feature = "__isolation-docs"))]
  70. fn map_isolation(
  71. _options: &AssetOptions,
  72. dir: PathBuf,
  73. ) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> Result<(), EmbeddedAssetsError> {
  74. move |_key, path, input, _csp_hashes| {
  75. if path.extension() == Some(OsStr::new("html")) {
  76. let mut isolation_html =
  77. tauri_utils::html::parse(String::from_utf8_lossy(input).into_owned());
  78. // this is appended, so no need to reverse order it
  79. tauri_utils::html::inject_codegen_isolation_script(&mut isolation_html);
  80. // temporary workaround for windows not loading assets
  81. tauri_utils::html::inline_isolation(&mut isolation_html, &dir);
  82. *input = isolation_html.to_string().as_bytes().to_vec()
  83. }
  84. Ok(())
  85. }
  86. }
  87. /// Build a `tauri::Context` for including in application code.
  88. pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsError> {
  89. let ContextData {
  90. dev,
  91. config,
  92. config_parent,
  93. root,
  94. } = data;
  95. let mut options = AssetOptions::new(config.tauri.pattern.clone())
  96. .freeze_prototype(config.tauri.security.freeze_prototype);
  97. let csp = if dev {
  98. config
  99. .tauri
  100. .security
  101. .dev_csp
  102. .clone()
  103. .or_else(|| config.tauri.security.csp.clone())
  104. } else {
  105. config.tauri.security.csp.clone()
  106. };
  107. if csp.is_some() {
  108. options = options.with_csp();
  109. }
  110. let app_url = if dev {
  111. &config.build.dev_path
  112. } else {
  113. &config.build.dist_dir
  114. };
  115. let assets = match app_url {
  116. AppUrl::Url(url) => match url {
  117. WindowUrl::External(_) => Default::default(),
  118. WindowUrl::App(path) => {
  119. if path.components().count() == 0 {
  120. panic!(
  121. "The `{}` configuration cannot be empty",
  122. if dev { "devPath" } else { "distDir" }
  123. )
  124. }
  125. let assets_path = config_parent.join(path);
  126. if !assets_path.exists() {
  127. panic!(
  128. "The `{}` configuration is set to `{:?}` but this path doesn't exist",
  129. if dev { "devPath" } else { "distDir" },
  130. path
  131. )
  132. }
  133. EmbeddedAssets::new(assets_path, map_core_assets(&options))?
  134. }
  135. _ => unimplemented!(),
  136. },
  137. AppUrl::Files(files) => EmbeddedAssets::new(
  138. files
  139. .iter()
  140. .map(|p| config_parent.join(p))
  141. .collect::<Vec<_>>(),
  142. map_core_assets(&options),
  143. )?,
  144. _ => unimplemented!(),
  145. };
  146. // handle default window icons for Windows targets
  147. let default_window_icon = if cfg!(windows) {
  148. let icon_path = find_icon(
  149. &config,
  150. &config_parent,
  151. |i| i.ends_with(".ico"),
  152. "icons/icon.ico",
  153. );
  154. quote!(Some(include_bytes!(#icon_path).to_vec()))
  155. } else if cfg!(target_os = "linux") {
  156. let icon_path = find_icon(
  157. &config,
  158. &config_parent,
  159. |i| i.ends_with(".png"),
  160. "icons/icon.png",
  161. );
  162. quote!(Some(include_bytes!(#icon_path).to_vec()))
  163. } else {
  164. quote!(None)
  165. };
  166. let package_name = if let Some(product_name) = &config.package.product_name {
  167. quote!(#product_name.to_string())
  168. } else {
  169. quote!(env!("CARGO_PKG_NAME").to_string())
  170. };
  171. let package_version = if let Some(version) = &config.package.version {
  172. quote!(#version.to_string())
  173. } else {
  174. quote!(env!("CARGO_PKG_VERSION").to_string())
  175. };
  176. let package_info = quote!(
  177. #root::PackageInfo {
  178. name: #package_name,
  179. version: #package_version,
  180. authors: env!("CARGO_PKG_AUTHORS"),
  181. description: env!("CARGO_PKG_DESCRIPTION"),
  182. }
  183. );
  184. #[cfg(target_os = "linux")]
  185. let system_tray_icon = if let Some(tray) = &config.tauri.system_tray {
  186. let mut system_tray_icon_path = tray.icon_path.clone();
  187. system_tray_icon_path.set_extension("png");
  188. if dev {
  189. let system_tray_icon_path = config_parent
  190. .join(system_tray_icon_path)
  191. .display()
  192. .to_string();
  193. quote!(Some(#root::Icon::File(::std::path::PathBuf::from(#system_tray_icon_path))))
  194. } else {
  195. let system_tray_icon_file_path = system_tray_icon_path.to_string_lossy().to_string();
  196. quote!(
  197. Some(
  198. #root::Icon::File(
  199. #root::api::path::resolve_path(
  200. &#config,
  201. &#package_info,
  202. &Default::default(),
  203. #system_tray_icon_file_path,
  204. Some(#root::api::path::BaseDirectory::Resource)
  205. ).expect("failed to resolve resource dir")
  206. )
  207. )
  208. )
  209. }
  210. } else {
  211. quote!(None)
  212. };
  213. #[cfg(not(target_os = "linux"))]
  214. let system_tray_icon = if let Some(tray) = &config.tauri.system_tray {
  215. let mut system_tray_icon_path = tray.icon_path.clone();
  216. system_tray_icon_path.set_extension(if cfg!(windows) { "ico" } else { "png" });
  217. let system_tray_icon_path = config_parent
  218. .join(system_tray_icon_path)
  219. .display()
  220. .to_string();
  221. quote!(Some(#root::Icon::Raw(include_bytes!(#system_tray_icon_path).to_vec())))
  222. } else {
  223. quote!(None)
  224. };
  225. #[cfg(target_os = "macos")]
  226. let info_plist = {
  227. if dev {
  228. let info_plist_path = config_parent.join("Info.plist");
  229. if info_plist_path.exists() {
  230. let info_plist_path = info_plist_path.display().to_string();
  231. quote!({
  232. tauri::embed_plist::embed_info_plist!(#info_plist_path);
  233. })
  234. } else {
  235. quote!(())
  236. }
  237. } else {
  238. quote!(())
  239. }
  240. };
  241. #[cfg(not(target_os = "macos"))]
  242. let info_plist = quote!(());
  243. let pattern = match &options.pattern {
  244. PatternKind::Brownfield => quote!(#root::Pattern::Brownfield(std::marker::PhantomData)),
  245. #[cfg(any(feature = "isolation", feature = "__isolation-docs"))]
  246. PatternKind::Isolation { dir } => {
  247. let dir = config_parent.join(dir);
  248. if !dir.exists() {
  249. panic!(
  250. "The isolation dir configuration is set to `{:?}` but this path doesn't exist",
  251. dir
  252. )
  253. }
  254. let key = uuid::Uuid::new_v4().to_string();
  255. let assets = EmbeddedAssets::new(dir.clone(), map_isolation(&options, dir))?;
  256. let schema = options.isolation_schema;
  257. quote!(#root::Pattern::Isolation {
  258. assets: ::std::sync::Arc::new(#assets),
  259. schema: #schema.into(),
  260. key: #key.into(),
  261. crypto_keys: std::boxed::Box::new(::tauri::utils::pattern::isolation::Keys::new().expect("unable to generate cryptographically secure keys for Tauri \"Isolation\" Pattern")),
  262. })
  263. }
  264. };
  265. #[cfg(feature = "shell-scope")]
  266. let shell_scope_config = {
  267. use regex::Regex;
  268. use tauri_utils::config::ShellAllowlistOpen;
  269. let shell_scopes = get_allowed_clis(&root, &config.tauri.allowlist.shell.scope);
  270. let shell_scope_open = match &config.tauri.allowlist.shell.open {
  271. ShellAllowlistOpen::Flag(false) => quote!(::std::option::Option::None),
  272. ShellAllowlistOpen::Flag(true) => {
  273. quote!(::std::option::Option::Some(#root::regex::Regex::new("^https?://").unwrap()))
  274. }
  275. ShellAllowlistOpen::Validate(regex) => match Regex::new(regex) {
  276. Ok(_) => quote!(::std::option::Option::Some(#root::regex::Regex::new(#regex).unwrap())),
  277. Err(error) => {
  278. let error = error.to_string();
  279. quote!({
  280. compile_error!(#error);
  281. ::std::option::Option::Some(#root::regex::Regex::new(#regex).unwrap())
  282. })
  283. }
  284. },
  285. _ => panic!("unknown shell open format, unable to prepare"),
  286. };
  287. quote!(#root::ShellScopeConfig {
  288. open: #shell_scope_open,
  289. scopes: #shell_scopes
  290. })
  291. };
  292. #[cfg(not(feature = "shell-scope"))]
  293. let shell_scope_config = quote!();
  294. Ok(quote!(#root::Context::new(
  295. #config,
  296. ::std::sync::Arc::new(#assets),
  297. #default_window_icon,
  298. #system_tray_icon,
  299. #package_info,
  300. #info_plist,
  301. #pattern,
  302. #shell_scope_config
  303. )))
  304. }
  305. fn find_icon<F: Fn(&&String) -> bool>(
  306. config: &Config,
  307. config_parent: &Path,
  308. predicate: F,
  309. default: &str,
  310. ) -> String {
  311. let icon_path = config
  312. .tauri
  313. .bundle
  314. .icon
  315. .iter()
  316. .find(|i| predicate(i))
  317. .cloned()
  318. .unwrap_or_else(|| default.to_string());
  319. config_parent.join(icon_path).display().to_string()
  320. }
  321. #[cfg(feature = "shell-scope")]
  322. fn get_allowed_clis(root: &TokenStream, scope: &ShellAllowlistScope) -> TokenStream {
  323. let commands = scope
  324. .0
  325. .iter()
  326. .map(|scope| {
  327. let sidecar = &scope.sidecar;
  328. let name = &scope.name;
  329. let name = quote!(#name.into());
  330. let command = scope.command.to_string_lossy();
  331. let command = quote!(::std::path::PathBuf::from(#command));
  332. let args = match &scope.args {
  333. ShellAllowedArgs::Flag(true) => quote!(::std::option::Option::None),
  334. ShellAllowedArgs::Flag(false) => quote!(::std::option::Option::Some(::std::vec![])),
  335. ShellAllowedArgs::List(list) => {
  336. let list = list.iter().map(|arg| match arg {
  337. ShellAllowedArg::Fixed(fixed) => {
  338. quote!(#root::scope::ShellScopeAllowedArg::Fixed(#fixed.into()))
  339. }
  340. ShellAllowedArg::Var { validator } => {
  341. let validator = match regex::Regex::new(validator) {
  342. Ok(regex) => {
  343. let regex = regex.as_str();
  344. quote!(#root::regex::Regex::new(#regex).unwrap())
  345. }
  346. Err(error) => {
  347. let error = error.to_string();
  348. quote!({
  349. compile_error!(#error);
  350. #root::regex::Regex::new(#validator).unwrap()
  351. })
  352. }
  353. };
  354. quote!(#root::scope::ShellScopeAllowedArg::Var { validator: #validator })
  355. }
  356. _ => panic!("unknown shell scope arg, unable to prepare"),
  357. });
  358. quote!(::std::option::Option::Some(::std::vec![#(#list),*]))
  359. }
  360. _ => panic!("unknown shell scope command, unable to prepare"),
  361. };
  362. (
  363. quote!(#name),
  364. quote!(
  365. #root::scope::ShellScopeAllowedCommand {
  366. command: #command,
  367. args: #args,
  368. sidecar: #sidecar,
  369. }
  370. ),
  371. )
  372. })
  373. .collect::<Vec<_>>();
  374. if commands.is_empty() {
  375. quote!(::std::collections::HashMap::new())
  376. } else {
  377. let insertions = commands
  378. .iter()
  379. .map(|(name, value)| quote!(hashmap.insert(#name, #value);));
  380. quote!({
  381. let mut hashmap = ::std::collections::HashMap::new();
  382. #(#insertions)*
  383. hashmap
  384. })
  385. }
  386. }