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 {
195    return Err(crate::Error::Architecture);
196  };
197
198  let os = if cfg!(target_os = "linux") {
199    "unknown-linux"
200  } else if cfg!(target_os = "macos") {
201    "apple-darwin"
202  } else if cfg!(target_os = "windows") {
203    "pc-windows"
204  } else if cfg!(target_os = "freebsd") {
205    "unknown-freebsd"
206  } else {
207    return Err(crate::Error::Os);
208  };
209
210  let os = if cfg!(target_os = "macos") || cfg!(target_os = "freebsd") {
211    String::from(os)
212  } else {
213    let env = if cfg!(target_env = "gnu") {
214      "gnu"
215    } else if cfg!(target_env = "musl") {
216      "musl"
217    } else if cfg!(target_env = "msvc") {
218      "msvc"
219    } else {
220      return Err(crate::Error::Environment);
221    };
222
223    format!("{os}-{env}")
224  };
225
226  Ok(format!("{arch}-{os}"))
227}
228
229#[cfg(all(not(test), not(target_os = "android")))]
230fn is_cargo_output_directory(path: &std::path::Path) -> bool {
231  path.join(".cargo-lock").exists()
232}
233
234#[cfg(test)]
235const CARGO_OUTPUT_DIRECTORIES: &[&str] = &["debug", "release", "custom-profile"];
236
237#[cfg(test)]
238fn is_cargo_output_directory(path: &std::path::Path) -> bool {
239  let last_component = path
240    .components()
241    .last()
242    .unwrap()
243    .as_os_str()
244    .to_str()
245    .unwrap();
246  CARGO_OUTPUT_DIRECTORIES
247    .iter()
248    .any(|dirname| &last_component == dirname)
249}
250
251/// Computes the resource directory of the current environment.
252///
253/// On Windows, it's the path to the executable.
254///
255/// On Linux, when running in an AppImage the `APPDIR` variable will be set to
256/// the mounted location of the app, and the resource dir will be
257/// `${APPDIR}/usr/lib/${exe_name}`. If not running in an AppImage, the path is
258/// `/usr/lib/${exe_name}`.  When running the app from
259/// `src-tauri/target/(debug|release)/`, the path is
260/// `${exe_dir}/../lib/${exe_name}`.
261///
262/// On MacOS, it's `${exe_dir}../Resources` (inside .app).
263///
264/// On iOS, it's `${exe_dir}/assets`.
265///
266/// Android uses a special URI prefix that is resolved by the Tauri file system plugin `asset://localhost/`
267pub fn resource_dir(package_info: &PackageInfo, env: &Env) -> crate::Result<PathBuf> {
268  #[cfg(target_os = "android")]
269  return resource_dir_android(package_info, env);
270  #[cfg(not(target_os = "android"))]
271  {
272    let exe = current_exe()?;
273    resource_dir_from(exe, package_info, env)
274  }
275}
276
277#[cfg(target_os = "android")]
278fn resource_dir_android(_package_info: &PackageInfo, _env: &Env) -> crate::Result<PathBuf> {
279  Ok(PathBuf::from(ANDROID_ASSET_PROTOCOL_URI_PREFIX))
280}
281
282#[cfg(not(target_os = "android"))]
283#[allow(unused_variables)]
284fn resource_dir_from<P: AsRef<std::path::Path>>(
285  exe: P,
286  package_info: &PackageInfo,
287  env: &Env,
288) -> crate::Result<PathBuf> {
289  let exe_dir = exe.as_ref().parent().expect("failed to get exe directory");
290  let curr_dir = exe_dir.display().to_string();
291
292  let parts: Vec<&str> = curr_dir.split(std::path::MAIN_SEPARATOR).collect();
293  let len = parts.len();
294
295  // Check if running from the Cargo output directory, which means it's an executable in a development machine
296  // We check if the binary is inside a `target` folder which can be either `target/$profile` or `target/$triple/$profile`
297  // and see if there's a .cargo-lock file along the executable
298  // This ensures the check is safer so it doesn't affect apps in production
299  // Windows also includes the resources in the executable folder so we check that too
300  if cfg!(target_os = "windows")
301    || ((len >= 2 && parts[len - 2] == "target") || (len >= 3 && parts[len - 3] == "target"))
302      && is_cargo_output_directory(exe_dir)
303  {
304    return Ok(exe_dir.to_path_buf());
305  }
306
307  #[allow(unused_mut, unused_assignments)]
308  let mut res = Err(crate::Error::UnsupportedPlatform);
309
310  #[cfg(target_os = "linux")]
311  {
312    // (canonicalize checks for existence, so there's no need for an extra check)
313    res = if let Ok(bundle_dir) = exe_dir
314      .join(format!("../lib/{}", package_info.name))
315      .canonicalize()
316    {
317      Ok(bundle_dir)
318    } else if let Some(appdir) = &env.appdir {
319      let appdir: &std::path::Path = appdir.as_ref();
320      Ok(PathBuf::from(format!(
321        "{}/usr/lib/{}",
322        appdir.display(),
323        package_info.name
324      )))
325    } else {
326      // running bundle
327      Ok(PathBuf::from(format!("/usr/lib/{}", package_info.name)))
328    };
329  }
330
331  #[cfg(target_os = "macos")]
332  {
333    res = exe_dir
334      .join("../Resources")
335      .canonicalize()
336      .map_err(Into::into);
337  }
338
339  #[cfg(target_os = "ios")]
340  {
341    res = exe_dir.join("assets").canonicalize().map_err(Into::into);
342  }
343
344  res
345}
346
347#[cfg(feature = "build")]
348mod build {
349  use proc_macro2::TokenStream;
350  use quote::{quote, ToTokens, TokenStreamExt};
351
352  use super::*;
353
354  impl ToTokens for Target {
355    fn to_tokens(&self, tokens: &mut TokenStream) {
356      let prefix = quote! { ::tauri::utils::platform::Target };
357
358      tokens.append_all(match self {
359        Self::MacOS => quote! { #prefix::MacOS },
360        Self::Linux => quote! { #prefix::Linux },
361        Self::Windows => quote! { #prefix::Windows },
362        Self::Android => quote! { #prefix::Android },
363        Self::Ios => quote! { #prefix::Ios },
364      });
365    }
366  }
367}
368
369#[cfg(test)]
370mod tests {
371  use std::path::PathBuf;
372
373  use crate::{Env, PackageInfo};
374
375  #[test]
376  fn resolve_resource_dir() {
377    let package_info = PackageInfo {
378      name: "MyApp".into(),
379      version: "1.0.0".parse().unwrap(),
380      authors: "",
381      description: "",
382      crate_name: "my-app",
383    };
384    let env = Env::default();
385
386    let path = PathBuf::from("/path/to/target/aarch64-apple-darwin/debug/app");
387    let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
388    assert_eq!(resource_dir, path.parent().unwrap());
389
390    let path = PathBuf::from("/path/to/target/custom-profile/app");
391    let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
392    assert_eq!(resource_dir, path.parent().unwrap());
393
394    let path = PathBuf::from("/path/to/target/release/app");
395    let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
396    assert_eq!(resource_dir, path.parent().unwrap());
397
398    let path = PathBuf::from("/path/to/target/unknown-profile/app");
399    #[allow(clippy::needless_borrows_for_generic_args)]
400    let resource_dir = super::resource_dir_from(&path, &package_info, &env);
401    #[cfg(target_os = "macos")]
402    assert!(resource_dir.is_err());
403    #[cfg(target_os = "linux")]
404    assert_eq!(resource_dir.unwrap(), PathBuf::from("/usr/lib/my-app"));
405    #[cfg(windows)]
406    assert_eq!(resource_dir.unwrap(), path.parent().unwrap());
407  }
408}