tauri_utils/
platform.rs

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