Skip to main content

tauri/path/
mod.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use std::{
6  path::{Component, Display, Path, PathBuf},
7  str::FromStr,
8};
9
10use crate::Runtime;
11
12use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize};
13use serde_repr::{Deserialize_repr, Serialize_repr};
14
15pub(crate) mod plugin;
16
17use crate::error::*;
18
19#[cfg(target_os = "android")]
20mod android;
21#[cfg(not(target_os = "android"))]
22mod desktop;
23
24#[cfg(target_os = "android")]
25pub use android::PathResolver;
26#[cfg(not(target_os = "android"))]
27pub use desktop::PathResolver;
28
29/// A wrapper for [`PathBuf`] that prevents path traversal.
30///
31/// # Examples
32///
33/// ```
34/// # use tauri::path::SafePathBuf;
35/// assert!(SafePathBuf::new("../secret.txt".into()).is_err());
36/// assert!(SafePathBuf::new("/home/user/stuff/../secret.txt".into()).is_err());
37///
38/// assert!(SafePathBuf::new("./file.txt".into()).is_ok());
39/// assert!(SafePathBuf::new("/home/user/secret.txt".into()).is_ok());
40/// ```
41#[derive(Clone, Debug, Serialize)]
42pub struct SafePathBuf(PathBuf);
43
44impl SafePathBuf {
45  /// Validates the path for directory traversal vulnerabilities and returns a new [`SafePathBuf`] instance if it is safe.
46  pub fn new(path: PathBuf) -> std::result::Result<Self, &'static str> {
47    if path.components().any(|x| matches!(x, Component::ParentDir)) {
48      Err("cannot traverse directory, rewrite the path without the use of `../`")
49    } else {
50      Ok(Self(path))
51    }
52  }
53
54  /// Returns an object that implements [`std::fmt::Display`] for safely printing paths.
55  ///
56  /// See [`PathBuf#method.display`] for more information.
57  pub fn display(&self) -> Display<'_> {
58    self.0.display()
59  }
60}
61
62impl AsRef<Path> for SafePathBuf {
63  fn as_ref(&self) -> &Path {
64    self.0.as_ref()
65  }
66}
67
68impl FromStr for SafePathBuf {
69  type Err = &'static str;
70
71  fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
72    Self::new(s.into())
73  }
74}
75
76impl From<SafePathBuf> for PathBuf {
77  fn from(path: SafePathBuf) -> Self {
78    path.0
79  }
80}
81
82impl<'de> Deserialize<'de> for SafePathBuf {
83  fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
84  where
85    D: Deserializer<'de>,
86  {
87    let path = PathBuf::deserialize(deserializer)?;
88    SafePathBuf::new(path).map_err(DeError::custom)
89  }
90}
91
92/// A base directory for a path.
93///
94/// The base directory is the optional root of a file system operation.
95/// If informed by the API call, all paths will be relative to the path of the given directory.
96///
97/// For more information, check the [`dirs` documentation](https://docs.rs/dirs/).
98#[derive(Serialize_repr, Deserialize_repr, Clone, Copy, Debug)]
99#[repr(u16)]
100#[non_exhaustive]
101pub enum BaseDirectory {
102  /// The Audio directory.
103  /// Resolves to [`crate::path::PathResolver::audio_dir`].
104  Audio = 1,
105  /// The Cache directory.
106  /// Resolves to [`crate::path::PathResolver::cache_dir`].
107  Cache = 2,
108  /// The Config directory.
109  /// Resolves to [`crate::path::PathResolver::config_dir`].
110  Config = 3,
111  /// The Data directory.
112  /// Resolves to [`crate::path::PathResolver::data_dir`].
113  Data = 4,
114  /// The LocalData directory.
115  /// Resolves to [`crate::path::PathResolver::local_data_dir`].
116  LocalData = 5,
117  /// The Document directory.
118  /// Resolves to [`crate::path::PathResolver::document_dir`].
119  Document = 6,
120  /// The Download directory.
121  /// Resolves to [`crate::path::PathResolver::download_dir`].
122  Download = 7,
123  /// The Picture directory.
124  /// Resolves to [`crate::path::PathResolver::picture_dir`].
125  Picture = 8,
126  /// The Public directory.
127  /// Resolves to [`crate::path::PathResolver::public_dir`].
128  Public = 9,
129  /// The Video directory.
130  /// Resolves to [`crate::path::PathResolver::video_dir`].
131  Video = 10,
132  /// The Resource directory.
133  /// Resolves to [`crate::path::PathResolver::resource_dir`].
134  Resource = 11,
135  /// A temporary directory.
136  /// Resolves to [`std::env::temp_dir`].
137  Temp = 12,
138  /// The default app config directory.
139  /// Resolves to [`BaseDirectory::Config`]`/{bundle_identifier}`.
140  AppConfig = 13,
141  /// The default app data directory.
142  /// Resolves to [`BaseDirectory::Data`]`/{bundle_identifier}`.
143  AppData = 14,
144  /// The default app local data directory.
145  /// Resolves to [`BaseDirectory::LocalData`]`/{bundle_identifier}`.
146  AppLocalData = 15,
147  /// The default app cache directory.
148  /// Resolves to [`BaseDirectory::Cache`]`/{bundle_identifier}`.
149  AppCache = 16,
150  /// The default app log directory.
151  /// Resolves to [`BaseDirectory::Home`]`/Library/Logs/{bundle_identifier}` on macOS
152  /// and [`BaseDirectory::Config`]`/{bundle_identifier}/logs` on linux and Windows.
153  AppLog = 17,
154  /// The Desktop directory.
155  /// Resolves to [`crate::path::PathResolver::desktop_dir`].
156  #[cfg(not(target_os = "android"))]
157  Desktop = 18,
158  /// The Executable directory.
159  /// Resolves to [`crate::path::PathResolver::executable_dir`].
160  #[cfg(not(target_os = "android"))]
161  Executable = 19,
162  /// The Font directory.
163  /// Resolves to [`crate::path::PathResolver::font_dir`].
164  #[cfg(not(target_os = "android"))]
165  Font = 20,
166  /// The Home directory.
167  /// Resolves to [`crate::path::PathResolver::home_dir`].
168  Home = 21,
169  /// The Runtime directory.
170  /// Resolves to [`crate::path::PathResolver::runtime_dir`].
171  #[cfg(not(target_os = "android"))]
172  Runtime = 22,
173  /// The Template directory.
174  /// Resolves to [`crate::path::PathResolver::template_dir`].
175  #[cfg(not(target_os = "android"))]
176  Template = 23,
177}
178
179impl BaseDirectory {
180  /// Gets the variable that represents this [`BaseDirectory`] for string paths.
181  pub fn variable(self) -> &'static str {
182    match self {
183      Self::Audio => "$AUDIO",
184      Self::Cache => "$CACHE",
185      Self::Config => "$CONFIG",
186      Self::Data => "$DATA",
187      Self::LocalData => "$LOCALDATA",
188      Self::Document => "$DOCUMENT",
189      Self::Download => "$DOWNLOAD",
190      Self::Picture => "$PICTURE",
191      Self::Public => "$PUBLIC",
192      Self::Video => "$VIDEO",
193      Self::Resource => "$RESOURCE",
194      Self::Temp => "$TEMP",
195      Self::AppConfig => "$APPCONFIG",
196      Self::AppData => "$APPDATA",
197      Self::AppLocalData => "$APPLOCALDATA",
198      Self::AppCache => "$APPCACHE",
199      Self::AppLog => "$APPLOG",
200      Self::Home => "$HOME",
201
202      #[cfg(not(target_os = "android"))]
203      Self::Desktop => "$DESKTOP",
204      #[cfg(not(target_os = "android"))]
205      Self::Executable => "$EXE",
206      #[cfg(not(target_os = "android"))]
207      Self::Font => "$FONT",
208      #[cfg(not(target_os = "android"))]
209      Self::Runtime => "$RUNTIME",
210      #[cfg(not(target_os = "android"))]
211      Self::Template => "$TEMPLATE",
212    }
213  }
214
215  /// Gets the [`BaseDirectory`] associated with the given variable, or [`None`] if the variable doesn't match any.
216  pub fn from_variable(variable: &str) -> Option<Self> {
217    let res = match variable {
218      "$AUDIO" => Self::Audio,
219      "$CACHE" => Self::Cache,
220      "$CONFIG" => Self::Config,
221      "$DATA" => Self::Data,
222      "$LOCALDATA" => Self::LocalData,
223      "$DOCUMENT" => Self::Document,
224      "$DOWNLOAD" => Self::Download,
225
226      "$PICTURE" => Self::Picture,
227      "$PUBLIC" => Self::Public,
228      "$VIDEO" => Self::Video,
229      "$RESOURCE" => Self::Resource,
230      "$TEMP" => Self::Temp,
231      "$APPCONFIG" => Self::AppConfig,
232      "$APPDATA" => Self::AppData,
233      "$APPLOCALDATA" => Self::AppLocalData,
234      "$APPCACHE" => Self::AppCache,
235      "$APPLOG" => Self::AppLog,
236      "$HOME" => Self::Home,
237
238      #[cfg(not(target_os = "android"))]
239      "$DESKTOP" => Self::Desktop,
240      #[cfg(not(target_os = "android"))]
241      "$EXE" => Self::Executable,
242      #[cfg(not(target_os = "android"))]
243      "$FONT" => Self::Font,
244      #[cfg(not(target_os = "android"))]
245      "$RUNTIME" => Self::Runtime,
246      #[cfg(not(target_os = "android"))]
247      "$TEMPLATE" => Self::Template,
248
249      _ => return None,
250    };
251    Some(res)
252  }
253}
254
255impl<R: Runtime> PathResolver<R> {
256  /// Resolves the path with the base directory.
257  ///
258  /// # Examples
259  ///
260  /// ```rust,no_run
261  /// use tauri::{path::BaseDirectory, Manager};
262  /// tauri::Builder::default()
263  ///   .setup(|app| {
264  ///     let path = app.path().resolve("path/to/something", BaseDirectory::Config)?;
265  ///     assert_eq!(path.to_str().unwrap(), "/home/${whoami}/.config/path/to/something");
266  ///     Ok(())
267  ///   });
268  /// ```
269  pub fn resolve<P: AsRef<Path>>(&self, path: P, base_directory: BaseDirectory) -> Result<PathBuf> {
270    resolve_path::<R>(self, base_directory, Some(path.as_ref().to_path_buf()))
271  }
272
273  /// Parse the given path, resolving a [`BaseDirectory`] variable if the path starts with one.
274  ///
275  /// # Examples
276  ///
277  /// ```rust,no_run
278  /// use tauri::Manager;
279  /// tauri::Builder::default()
280  ///   .setup(|app| {
281  ///     let path = app.path().parse("$HOME/.bashrc")?;
282  ///     assert_eq!(path.to_str().unwrap(), "/home/${whoami}/.bashrc");
283  ///     Ok(())
284  ///   });
285  /// ```
286  pub fn parse<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
287    let mut p = PathBuf::new();
288    let mut components = path.as_ref().components();
289    match components.next() {
290      Some(Component::Normal(str)) => {
291        if let Some(base_directory) = BaseDirectory::from_variable(&str.to_string_lossy()) {
292          p.push(resolve_path::<R>(self, base_directory, None)?);
293        } else {
294          p.push(str);
295        }
296      }
297      Some(component) => p.push(component),
298      None => (),
299    }
300
301    for component in components {
302      if let Component::ParentDir = component {
303        continue;
304      }
305      p.push(component);
306    }
307
308    Ok(p)
309  }
310}
311
312fn resolve_path<R: Runtime>(
313  resolver: &PathResolver<R>,
314  directory: BaseDirectory,
315  path: Option<PathBuf>,
316) -> Result<PathBuf> {
317  let resolve_resource = matches!(directory, BaseDirectory::Resource);
318  let mut base_dir_path = match directory {
319    BaseDirectory::Audio => resolver.audio_dir(),
320    BaseDirectory::Cache => resolver.cache_dir(),
321    BaseDirectory::Config => resolver.config_dir(),
322    BaseDirectory::Data => resolver.data_dir(),
323    BaseDirectory::LocalData => resolver.local_data_dir(),
324    BaseDirectory::Document => resolver.document_dir(),
325    BaseDirectory::Download => resolver.download_dir(),
326    BaseDirectory::Picture => resolver.picture_dir(),
327    BaseDirectory::Public => resolver.public_dir(),
328    BaseDirectory::Video => resolver.video_dir(),
329    BaseDirectory::Resource => resolver.resource_dir(),
330    BaseDirectory::Temp => resolver.temp_dir(),
331    BaseDirectory::AppConfig => resolver.app_config_dir(),
332    BaseDirectory::AppData => resolver.app_data_dir(),
333    BaseDirectory::AppLocalData => resolver.app_local_data_dir(),
334    BaseDirectory::AppCache => resolver.app_cache_dir(),
335    BaseDirectory::AppLog => resolver.app_log_dir(),
336    BaseDirectory::Home => resolver.home_dir(),
337    #[cfg(not(target_os = "android"))]
338    BaseDirectory::Desktop => resolver.desktop_dir(),
339    #[cfg(not(target_os = "android"))]
340    BaseDirectory::Executable => resolver.executable_dir(),
341    #[cfg(not(target_os = "android"))]
342    BaseDirectory::Font => resolver.font_dir(),
343    #[cfg(not(target_os = "android"))]
344    BaseDirectory::Runtime => resolver.runtime_dir(),
345    #[cfg(not(target_os = "android"))]
346    BaseDirectory::Template => resolver.template_dir(),
347  }?;
348
349  if let Some(path) = path {
350    // use the same path resolution mechanism as the bundler's resource injection algorithm
351    if resolve_resource {
352      let mut resource_path = PathBuf::new();
353      for component in path.components() {
354        match component {
355          Component::Prefix(_) => {}
356          Component::RootDir => resource_path.push("_root_"),
357          Component::CurDir => {}
358          Component::ParentDir => resource_path.push("_up_"),
359          Component::Normal(p) => resource_path.push(p),
360        }
361      }
362      base_dir_path.push(resource_path);
363    } else {
364      base_dir_path.push(path);
365    }
366  }
367
368  Ok(base_dir_path)
369}
370
371#[cfg(test)]
372mod test {
373  use super::SafePathBuf;
374  use quickcheck::{Arbitrary, Gen};
375
376  use std::path::PathBuf;
377
378  impl Arbitrary for SafePathBuf {
379    fn arbitrary(g: &mut Gen) -> Self {
380      Self(PathBuf::arbitrary(g))
381    }
382
383    fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
384      Box::new(self.0.shrink().map(SafePathBuf))
385    }
386  }
387}