platform.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. //! Platform helper functions.
  5. use std::{
  6. fmt::Display,
  7. path::{Path, PathBuf, MAIN_SEPARATOR},
  8. };
  9. use serde::{Deserialize, Serialize};
  10. use crate::{Env, PackageInfo};
  11. mod starting_binary;
  12. /// Platform target.
  13. #[derive(PartialEq, Eq, Copy, Debug, Clone, Serialize, Deserialize)]
  14. #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
  15. #[serde(rename_all = "camelCase")]
  16. pub enum Target {
  17. /// MacOS.
  18. #[serde(rename = "macOS")]
  19. MacOS,
  20. /// Windows.
  21. Windows,
  22. /// Linux.
  23. Linux,
  24. /// Android.
  25. Android,
  26. /// iOS.
  27. #[serde(rename = "iOS")]
  28. Ios,
  29. }
  30. impl Display for Target {
  31. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  32. write!(
  33. f,
  34. "{}",
  35. match self {
  36. Self::MacOS => "macOS",
  37. Self::Windows => "windows",
  38. Self::Linux => "linux",
  39. Self::Android => "android",
  40. Self::Ios => "iOS",
  41. }
  42. )
  43. }
  44. }
  45. impl Target {
  46. /// Parses the target from the given target triple.
  47. pub fn from_triple(target: &str) -> Self {
  48. if target.contains("darwin") {
  49. Self::MacOS
  50. } else if target.contains("windows") {
  51. Self::Windows
  52. } else if target.contains("android") {
  53. Self::Android
  54. } else if target.contains("ios") {
  55. Self::Ios
  56. } else {
  57. Self::Linux
  58. }
  59. }
  60. /// Gets the current build target.
  61. pub fn current() -> Self {
  62. if cfg!(target_os = "macos") {
  63. Self::MacOS
  64. } else if cfg!(target_os = "windows") {
  65. Self::Windows
  66. } else if cfg!(target_os = "ios") {
  67. Self::Ios
  68. } else if cfg!(target_os = "android") {
  69. Self::Android
  70. } else {
  71. Self::Linux
  72. }
  73. }
  74. /// Whether the target is mobile or not.
  75. pub fn is_mobile(&self) -> bool {
  76. matches!(self, Target::Android | Target::Ios)
  77. }
  78. /// Whether the target is desktop or not.
  79. pub fn is_desktop(&self) -> bool {
  80. !self.is_mobile()
  81. }
  82. }
  83. /// Retrieves the currently running binary's path, taking into account security considerations.
  84. ///
  85. /// The path is cached as soon as possible (before even `main` runs) and that value is returned
  86. /// repeatedly instead of fetching the path every time. It is possible for the path to not be found,
  87. /// or explicitly disabled (see following macOS specific behavior).
  88. ///
  89. /// # Platform-specific behavior
  90. ///
  91. /// On `macOS`, this function will return an error if the original path contained any symlinks
  92. /// due to less protection on macOS regarding symlinks. This behavior can be disabled by setting the
  93. /// `process-relaunch-dangerous-allow-symlink-macos` feature, although it is *highly discouraged*.
  94. ///
  95. /// # Security
  96. ///
  97. /// If the above platform-specific behavior does **not** take place, this function uses the
  98. /// following resolution.
  99. ///
  100. /// We canonicalize the path we received from [`std::env::current_exe`] to resolve any soft links.
  101. /// This avoids the usual issue of needing the file to exist at the passed path because a valid
  102. /// current executable result for our purpose should always exist. Notably,
  103. /// [`std::env::current_exe`] also has a security section that goes over a theoretical attack using
  104. /// hard links. Let's cover some specific topics that relate to different ways an attacker might
  105. /// try to trick this function into returning the wrong binary path.
  106. ///
  107. /// ## Symlinks ("Soft Links")
  108. ///
  109. /// [`std::path::Path::canonicalize`] is used to resolve symbolic links to the original path,
  110. /// including nested symbolic links (`link2 -> link1 -> bin`). On macOS, any results that include
  111. /// a symlink are rejected by default due to lesser symlink protections. This can be disabled,
  112. /// **although discouraged**, with the `process-relaunch-dangerous-allow-symlink-macos` feature.
  113. ///
  114. /// ## Hard Links
  115. ///
  116. /// A [Hard Link] is a named entry that points to a file in the file system.
  117. /// On most systems, this is what you would think of as a "file". The term is
  118. /// used on filesystems that allow multiple entries to point to the same file.
  119. /// The linked [Hard Link] Wikipedia page provides a decent overview.
  120. ///
  121. /// In short, unless the attacker was able to create the link with elevated
  122. /// permissions, it should generally not be possible for them to hard link
  123. /// to a file they do not have permissions to - with exception to possible
  124. /// operating system exploits.
  125. ///
  126. /// There are also some platform-specific information about this below.
  127. ///
  128. /// ### Windows
  129. ///
  130. /// Windows requires a permission to be set for the user to create a symlink
  131. /// or a hard link, regardless of ownership status of the target. Elevated
  132. /// permissions users have the ability to create them.
  133. ///
  134. /// ### macOS
  135. ///
  136. /// macOS allows for the creation of symlinks and hard links to any file.
  137. /// Accessing through those links will fail if the user who owns the links
  138. /// does not have the proper permissions on the original file.
  139. ///
  140. /// ### Linux
  141. ///
  142. /// Linux allows for the creation of symlinks to any file. Accessing the
  143. /// symlink will fail if the user who owns the symlink does not have the
  144. /// proper permissions on the original file.
  145. ///
  146. /// Linux additionally provides a kernel hardening feature since version
  147. /// 3.6 (30 September 2012). Most distributions since then have enabled
  148. /// the protection (setting `fs.protected_hardlinks = 1`) by default, which
  149. /// means that a vast majority of desktop Linux users should have it enabled.
  150. /// **The feature prevents the creation of hardlinks that the user does not own
  151. /// or have read/write access to.** [See the patch that enabled this].
  152. ///
  153. /// [Hard Link]: https://en.wikipedia.org/wiki/Hard_link
  154. /// [See the patch that enabled this]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=800179c9b8a1e796e441674776d11cd4c05d61d7
  155. pub fn current_exe() -> std::io::Result<PathBuf> {
  156. self::starting_binary::STARTING_BINARY.cloned()
  157. }
  158. /// Try to determine the current target triple.
  159. ///
  160. /// Returns a target triple (e.g. `x86_64-unknown-linux-gnu` or `i686-pc-windows-msvc`) or an
  161. /// `Error::Config` if the current config cannot be determined or is not some combination of the
  162. /// following values:
  163. /// `linux, mac, windows` -- `i686, x86, armv7` -- `gnu, musl, msvc`
  164. ///
  165. /// * Errors:
  166. /// * Unexpected system config
  167. pub fn target_triple() -> crate::Result<String> {
  168. let arch = if cfg!(target_arch = "x86") {
  169. "i686"
  170. } else if cfg!(target_arch = "x86_64") {
  171. "x86_64"
  172. } else if cfg!(target_arch = "arm") {
  173. "armv7"
  174. } else if cfg!(target_arch = "aarch64") {
  175. "aarch64"
  176. } else {
  177. return Err(crate::Error::Architecture);
  178. };
  179. let os = if cfg!(target_os = "linux") {
  180. "unknown-linux"
  181. } else if cfg!(target_os = "macos") {
  182. "apple-darwin"
  183. } else if cfg!(target_os = "windows") {
  184. "pc-windows"
  185. } else if cfg!(target_os = "freebsd") {
  186. "unknown-freebsd"
  187. } else {
  188. return Err(crate::Error::Os);
  189. };
  190. let os = if cfg!(target_os = "macos") || cfg!(target_os = "freebsd") {
  191. String::from(os)
  192. } else {
  193. let env = if cfg!(target_env = "gnu") {
  194. "gnu"
  195. } else if cfg!(target_env = "musl") {
  196. "musl"
  197. } else if cfg!(target_env = "msvc") {
  198. "msvc"
  199. } else {
  200. return Err(crate::Error::Environment);
  201. };
  202. format!("{os}-{env}")
  203. };
  204. Ok(format!("{arch}-{os}"))
  205. }
  206. #[cfg(not(test))]
  207. fn is_cargo_output_directory(path: &Path) -> bool {
  208. path.join(".cargo-lock").exists()
  209. }
  210. #[cfg(test)]
  211. const CARGO_OUTPUT_DIRECTORIES: &[&str] = &["debug", "release", "custom-profile"];
  212. #[cfg(test)]
  213. fn is_cargo_output_directory(path: &Path) -> bool {
  214. let last_component = path
  215. .components()
  216. .last()
  217. .unwrap()
  218. .as_os_str()
  219. .to_str()
  220. .unwrap();
  221. CARGO_OUTPUT_DIRECTORIES
  222. .iter()
  223. .any(|dirname| &last_component == dirname)
  224. }
  225. /// Computes the resource directory of the current environment.
  226. ///
  227. /// On Windows, it's the path to the executable.
  228. ///
  229. /// On Linux, when running in an AppImage the `APPDIR` variable will be set to
  230. /// the mounted location of the app, and the resource dir will be
  231. /// `${APPDIR}/usr/lib/${exe_name}`. If not running in an AppImage, the path is
  232. /// `/usr/lib/${exe_name}`. When running the app from
  233. /// `src-tauri/target/(debug|release)/`, the path is
  234. /// `${exe_dir}/../lib/${exe_name}`.
  235. ///
  236. /// On MacOS, it's `${exe_dir}../Resources` (inside .app).
  237. pub fn resource_dir(package_info: &PackageInfo, env: &Env) -> crate::Result<PathBuf> {
  238. let exe = current_exe()?;
  239. resource_dir_from(exe, package_info, env)
  240. }
  241. #[allow(unused_variables)]
  242. fn resource_dir_from<P: AsRef<Path>>(
  243. exe: P,
  244. package_info: &PackageInfo,
  245. env: &Env,
  246. ) -> crate::Result<PathBuf> {
  247. let exe_dir = exe.as_ref().parent().expect("failed to get exe directory");
  248. let curr_dir = exe_dir.display().to_string();
  249. let parts: Vec<&str> = curr_dir.split(MAIN_SEPARATOR).collect();
  250. let len = parts.len();
  251. // Check if running from the Cargo output directory, which means it's an executable in a development machine
  252. // We check if the binary is inside a `target` folder which can be either `target/$profile` or `target/$triple/$profile`
  253. // and see if there's a .cargo-lock file along the executable
  254. // This ensures the check is safer so it doesn't affect apps in production
  255. // Windows also includes the resources in the executable folder so we check that too
  256. if cfg!(target_os = "windows")
  257. || ((len >= 2 && parts[len - 2] == "target") || (len >= 3 && parts[len - 3] == "target"))
  258. && is_cargo_output_directory(exe_dir)
  259. {
  260. return Ok(exe_dir.to_path_buf());
  261. }
  262. #[allow(unused_mut, unused_assignments)]
  263. let mut res = Err(crate::Error::UnsupportedPlatform);
  264. #[cfg(target_os = "linux")]
  265. {
  266. res = if curr_dir.ends_with("/data/usr/bin") {
  267. // running from the deb bundle dir
  268. exe_dir
  269. .join(format!("../lib/{}", package_info.crate_name))
  270. .canonicalize()
  271. .map_err(Into::into)
  272. } else if let Some(appdir) = &env.appdir {
  273. let appdir: &std::path::Path = appdir.as_ref();
  274. Ok(PathBuf::from(format!(
  275. "{}/usr/lib/{}",
  276. appdir.display(),
  277. package_info.crate_name
  278. )))
  279. } else {
  280. // running bundle
  281. Ok(PathBuf::from(format!(
  282. "/usr/lib/{}",
  283. package_info.crate_name
  284. )))
  285. };
  286. }
  287. #[cfg(target_os = "macos")]
  288. {
  289. res = exe_dir
  290. .join("../Resources")
  291. .canonicalize()
  292. .map_err(Into::into);
  293. }
  294. res
  295. }
  296. #[cfg(feature = "build")]
  297. mod build {
  298. use proc_macro2::TokenStream;
  299. use quote::{quote, ToTokens, TokenStreamExt};
  300. use super::*;
  301. impl ToTokens for Target {
  302. fn to_tokens(&self, tokens: &mut TokenStream) {
  303. let prefix = quote! { ::tauri::utils::platform::Target };
  304. tokens.append_all(match self {
  305. Self::MacOS => quote! { #prefix::MacOS },
  306. Self::Linux => quote! { #prefix::Linux },
  307. Self::Windows => quote! { #prefix::Windows },
  308. Self::Android => quote! { #prefix::Android },
  309. Self::Ios => quote! { #prefix::Ios },
  310. });
  311. }
  312. }
  313. }
  314. #[cfg(test)]
  315. mod tests {
  316. use std::path::PathBuf;
  317. use crate::{Env, PackageInfo};
  318. #[test]
  319. fn resolve_resource_dir() {
  320. let package_info = PackageInfo {
  321. name: "MyApp".into(),
  322. version: "1.0.0".parse().unwrap(),
  323. authors: "",
  324. description: "",
  325. crate_name: "my-app",
  326. };
  327. let env = Env::default();
  328. let path = PathBuf::from("/path/to/target/aarch64-apple-darwin/debug/app");
  329. let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
  330. assert_eq!(resource_dir, path.parent().unwrap());
  331. let path = PathBuf::from("/path/to/target/custom-profile/app");
  332. let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
  333. assert_eq!(resource_dir, path.parent().unwrap());
  334. let path = PathBuf::from("/path/to/target/release/app");
  335. let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
  336. assert_eq!(resource_dir, path.parent().unwrap());
  337. let path = PathBuf::from("/path/to/target/unknown-profile/app");
  338. let resource_dir = super::resource_dir_from(&path, &package_info, &env);
  339. #[cfg(target_os = "macos")]
  340. assert!(resource_dir.is_err());
  341. #[cfg(target_os = "linux")]
  342. assert_eq!(resource_dir.unwrap(), PathBuf::from("/usr/lib/my-app"));
  343. #[cfg(windows)]
  344. assert_eq!(resource_dir.unwrap(), path.parent().unwrap());
  345. }
  346. }