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::{config::BundleType, 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    .next_back()
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/// ## Platform-specific
256///
257/// - **Windows:** Resolves to the directory that contains the main executable.
258/// - **Linux:** When running in an AppImage, the `APPDIR` variable will be set to
259///   the mounted location of the app, and the resource dir will be `${APPDIR}/usr/lib/${exe_name}`.
260///   If not running in an AppImage, the path is `/usr/lib/${exe_name}`.
261///   When running the app from `src-tauri/target/(debug|release)/`, the path is `${exe_dir}/../lib/${exe_name}`.
262/// - **macOS:** Resolves to `${exe_dir}/../Resources` (inside .app).
263/// - **iOS:** Resolves to `${exe_dir}/assets`.
264/// - **Android:** Currently the resources are stored in the APK as assets so it's not a normal file system path,
265///   we return a special URI prefix `asset://localhost/` here that can be used with the [file system plugin](https://tauri.app/plugin/file-system/),
266///   with that, you can read the files through [`FsExt::fs`](https://docs.rs/tauri-plugin-fs/latest/tauri_plugin_fs/trait.FsExt.html#tymethod.fs)
267///   like this: `app.fs().read_to_string(app.path().resource_dir().unwrap().join("resource"));`
268pub fn resource_dir(package_info: &PackageInfo, env: &Env) -> crate::Result<PathBuf> {
269  #[cfg(target_os = "android")]
270  return resource_dir_android(package_info, env);
271  #[cfg(not(target_os = "android"))]
272  {
273    let exe = current_exe()?;
274    resource_dir_from(exe, package_info, env)
275  }
276}
277
278#[cfg(target_os = "android")]
279fn resource_dir_android(_package_info: &PackageInfo, _env: &Env) -> crate::Result<PathBuf> {
280  Ok(PathBuf::from(ANDROID_ASSET_PROTOCOL_URI_PREFIX))
281}
282
283#[cfg(not(target_os = "android"))]
284#[allow(unused_variables)]
285fn resource_dir_from<P: AsRef<std::path::Path>>(
286  exe: P,
287  package_info: &PackageInfo,
288  env: &Env,
289) -> crate::Result<PathBuf> {
290  let exe_dir = exe.as_ref().parent().expect("failed to get exe directory");
291  let curr_dir = exe_dir.display().to_string();
292
293  let parts: Vec<&str> = curr_dir.split(std::path::MAIN_SEPARATOR).collect();
294  let len = parts.len();
295
296  // Check if running from the Cargo output directory, which means it's an executable in a development machine
297  // We check if the binary is inside a `target` folder which can be either `target/$profile` or `target/$triple/$profile`
298  // and see if there's a .cargo-lock file along the executable
299  // This ensures the check is safer so it doesn't affect apps in production
300  // Windows also includes the resources in the executable folder so we check that too
301  if cfg!(target_os = "windows")
302    || ((len >= 2 && parts[len - 2] == "target") || (len >= 3 && parts[len - 3] == "target"))
303      && is_cargo_output_directory(exe_dir)
304  {
305    return Ok(exe_dir.to_path_buf());
306  }
307
308  #[allow(unused_mut, unused_assignments)]
309  let mut res = Err(crate::Error::UnsupportedPlatform);
310
311  #[cfg(target_os = "linux")]
312  {
313    // (canonicalize checks for existence, so there's no need for an extra check)
314    res = if let Ok(bundle_dir) = exe_dir
315      .join(format!("../lib/{}", package_info.name))
316      .canonicalize()
317    {
318      Ok(bundle_dir)
319    } else if let Some(appdir) = &env.appdir {
320      let appdir: &std::path::Path = appdir.as_ref();
321      Ok(PathBuf::from(format!(
322        "{}/usr/lib/{}",
323        appdir.display(),
324        package_info.name
325      )))
326    } else {
327      // running bundle
328      Ok(PathBuf::from(format!("/usr/lib/{}", package_info.name)))
329    };
330  }
331
332  #[cfg(target_os = "macos")]
333  {
334    res = exe_dir
335      .join("../Resources")
336      .canonicalize()
337      .map_err(Into::into);
338  }
339
340  #[cfg(target_os = "ios")]
341  {
342    res = exe_dir.join("assets").canonicalize().map_err(Into::into);
343  }
344
345  res
346}
347
348// Variable holding the type of bundle the executable is stored in. This is modified by binary
349// patching during build
350#[used]
351#[no_mangle]
352#[cfg_attr(not(target_vendor = "apple"), link_section = ".taubndl")]
353#[cfg_attr(target_vendor = "apple", link_section = "__DATA,taubndl")]
354// Marked as `mut` becuase it could get optimized away without it,
355// see https://github.com/tauri-apps/tauri/pull/13812
356static mut __TAURI_BUNDLE_TYPE: &str = "UNK";
357
358/// Get the type of the bundle current binary is packaged in.
359/// If the bundle type is unknown, it returns [`Option::None`].
360pub fn bundle_type() -> Option<BundleType> {
361  unsafe {
362    match __TAURI_BUNDLE_TYPE {
363      "DEB" => Some(BundleType::Deb),
364      "RPM" => Some(BundleType::Rpm),
365      "APP" => Some(BundleType::AppImage),
366      "MSI" => Some(BundleType::Msi),
367      "NSS" => Some(BundleType::Nsis),
368      _ => {
369        if cfg!(target_os = "macos") {
370          Some(BundleType::App)
371        } else {
372          None
373        }
374      }
375    }
376  }
377}
378
379#[cfg(feature = "build")]
380mod build {
381  use proc_macro2::TokenStream;
382  use quote::{quote, ToTokens, TokenStreamExt};
383
384  use super::*;
385
386  impl ToTokens for Target {
387    fn to_tokens(&self, tokens: &mut TokenStream) {
388      let prefix = quote! { ::tauri::utils::platform::Target };
389
390      tokens.append_all(match self {
391        Self::MacOS => quote! { #prefix::MacOS },
392        Self::Linux => quote! { #prefix::Linux },
393        Self::Windows => quote! { #prefix::Windows },
394        Self::Android => quote! { #prefix::Android },
395        Self::Ios => quote! { #prefix::Ios },
396      });
397    }
398  }
399}
400
401#[cfg(test)]
402mod tests {
403  use std::path::PathBuf;
404
405  use crate::{Env, PackageInfo};
406
407  #[test]
408  fn resolve_resource_dir() {
409    let package_info = PackageInfo {
410      name: "MyApp".into(),
411      version: "1.0.0".parse().unwrap(),
412      authors: "",
413      description: "",
414      crate_name: "my-app",
415    };
416    let env = Env::default();
417
418    let path = PathBuf::from("/path/to/target/aarch64-apple-darwin/debug/app");
419    let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
420    assert_eq!(resource_dir, path.parent().unwrap());
421
422    let path = PathBuf::from("/path/to/target/custom-profile/app");
423    let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
424    assert_eq!(resource_dir, path.parent().unwrap());
425
426    let path = PathBuf::from("/path/to/target/release/app");
427    let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
428    assert_eq!(resource_dir, path.parent().unwrap());
429
430    let path = PathBuf::from("/path/to/target/unknown-profile/app");
431    #[allow(clippy::needless_borrows_for_generic_args)]
432    let resource_dir = super::resource_dir_from(&path, &package_info, &env);
433    #[cfg(target_os = "macos")]
434    assert!(resource_dir.is_err());
435    #[cfg(target_os = "linux")]
436    assert_eq!(resource_dir.unwrap(), PathBuf::from("/usr/lib/MyApp"));
437    #[cfg(windows)]
438    assert_eq!(resource_dir.unwrap(), path.parent().unwrap());
439  }
440}