123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
- // SPDX-License-Identifier: Apache-2.0
- // SPDX-License-Identifier: MIT
- use std::{
- path::{Component, Display, Path, PathBuf},
- str::FromStr,
- };
- use crate::Runtime;
- use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize};
- use serde_repr::{Deserialize_repr, Serialize_repr};
- pub(crate) mod plugin;
- use crate::error::*;
- #[cfg(target_os = "android")]
- mod android;
- #[cfg(not(target_os = "android"))]
- mod desktop;
- #[cfg(target_os = "android")]
- pub use android::PathResolver;
- #[cfg(not(target_os = "android"))]
- pub use desktop::PathResolver;
- /// A wrapper for [`PathBuf`] that prevents path traversal.
- #[derive(Clone, Debug, Serialize)]
- pub struct SafePathBuf(PathBuf);
- impl SafePathBuf {
- /// Validates the path for directory traversal vulnerabilities and returns a new [`SafePathBuf`] instance if it is safe.
- pub fn new(path: PathBuf) -> std::result::Result<Self, &'static str> {
- if path.components().any(|x| matches!(x, Component::ParentDir)) {
- Err("cannot traverse directory, rewrite the path without the use of `../`")
- } else {
- Ok(Self(path))
- }
- }
- /// Returns an object that implements [`std::fmt::Display`] for safely printing paths.
- ///
- /// See [`PathBuf#method.display`] for more information.
- pub fn display(&self) -> Display<'_> {
- self.0.display()
- }
- }
- impl AsRef<Path> for SafePathBuf {
- fn as_ref(&self) -> &Path {
- self.0.as_ref()
- }
- }
- impl FromStr for SafePathBuf {
- type Err = &'static str;
- fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
- Self::new(s.into())
- }
- }
- impl<'de> Deserialize<'de> for SafePathBuf {
- fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
- where
- D: Deserializer<'de>,
- {
- let path = PathBuf::deserialize(deserializer)?;
- SafePathBuf::new(path).map_err(DeError::custom)
- }
- }
- /// A base directory for a path.
- ///
- /// The base directory is the optional root of a file system operation.
- /// If informed by the API call, all paths will be relative to the path of the given directory.
- ///
- /// For more information, check the [`dirs` documentation](https://docs.rs/dirs/).
- #[derive(Serialize_repr, Deserialize_repr, Clone, Copy, Debug)]
- #[repr(u16)]
- #[non_exhaustive]
- pub enum BaseDirectory {
- /// The Audio directory.
- Audio = 1,
- /// The Cache directory.
- Cache,
- /// The Config directory.
- Config,
- /// The Data directory.
- Data,
- /// The LocalData directory.
- LocalData,
- /// The Document directory.
- Document,
- /// The Download directory.
- Download,
- /// The Picture directory.
- Picture,
- /// The Public directory.
- Public,
- /// The Video directory.
- Video,
- /// The Resource directory.
- Resource,
- /// A temporary directory. Resolves to [`std::env::temp_dir`].
- Temp,
- /// The default app config directory.
- /// Resolves to [`BaseDirectory::Config`]`/{bundle_identifier}`.
- AppConfig,
- /// The default app data directory.
- /// Resolves to [`BaseDirectory::Data`]`/{bundle_identifier}`.
- AppData,
- /// The default app local data directory.
- /// Resolves to [`BaseDirectory::LocalData`]`/{bundle_identifier}`.
- AppLocalData,
- /// The default app cache directory.
- /// Resolves to [`BaseDirectory::Cache`]`/{bundle_identifier}`.
- AppCache,
- /// The default app log directory.
- /// Resolves to [`BaseDirectory::Home`]`/Library/Logs/{bundle_identifier}` on macOS
- /// and [`BaseDirectory::Config`]`/{bundle_identifier}/logs` on linux and Windows.
- AppLog,
- /// The Home directory.
- Home,
- /// The Desktop directory.
- #[cfg(not(target_os = "android"))]
- Desktop,
- /// The Executable directory.
- #[cfg(not(target_os = "android"))]
- Executable,
- /// The Font directory.
- #[cfg(not(target_os = "android"))]
- Font,
- /// The Runtime directory.
- #[cfg(not(target_os = "android"))]
- Runtime,
- /// The Template directory.
- #[cfg(not(target_os = "android"))]
- Template,
- }
- impl BaseDirectory {
- /// Gets the variable that represents this [`BaseDirectory`] for string paths.
- pub fn variable(self) -> &'static str {
- match self {
- Self::Audio => "$AUDIO",
- Self::Cache => "$CACHE",
- Self::Config => "$CONFIG",
- Self::Data => "$DATA",
- Self::LocalData => "$LOCALDATA",
- Self::Document => "$DOCUMENT",
- Self::Download => "$DOWNLOAD",
- Self::Picture => "$PICTURE",
- Self::Public => "$PUBLIC",
- Self::Video => "$VIDEO",
- Self::Resource => "$RESOURCE",
- Self::Temp => "$TEMP",
- Self::AppConfig => "$APPCONFIG",
- Self::AppData => "$APPDATA",
- Self::AppLocalData => "$APPLOCALDATA",
- Self::AppCache => "$APPCACHE",
- Self::AppLog => "$APPLOG",
- Self::Home => "$HOME",
- #[cfg(not(target_os = "android"))]
- Self::Desktop => "$DESKTOP",
- #[cfg(not(target_os = "android"))]
- Self::Executable => "$EXE",
- #[cfg(not(target_os = "android"))]
- Self::Font => "$FONT",
- #[cfg(not(target_os = "android"))]
- Self::Runtime => "$RUNTIME",
- #[cfg(not(target_os = "android"))]
- Self::Template => "$TEMPLATE",
- }
- }
- /// Gets the [`BaseDirectory`] associated with the given variable, or [`None`] if the variable doesn't match any.
- pub fn from_variable(variable: &str) -> Option<Self> {
- let res = match variable {
- "$AUDIO" => Self::Audio,
- "$CACHE" => Self::Cache,
- "$CONFIG" => Self::Config,
- "$DATA" => Self::Data,
- "$LOCALDATA" => Self::LocalData,
- "$DOCUMENT" => Self::Document,
- "$DOWNLOAD" => Self::Download,
- "$PICTURE" => Self::Picture,
- "$PUBLIC" => Self::Public,
- "$VIDEO" => Self::Video,
- "$RESOURCE" => Self::Resource,
- "$TEMP" => Self::Temp,
- "$APPCONFIG" => Self::AppConfig,
- "$APPDATA" => Self::AppData,
- "$APPLOCALDATA" => Self::AppLocalData,
- "$APPCACHE" => Self::AppCache,
- "$APPLOG" => Self::AppLog,
- "$HOME" => Self::Home,
- #[cfg(not(target_os = "android"))]
- "$DESKTOP" => Self::Desktop,
- #[cfg(not(target_os = "android"))]
- "$EXE" => Self::Executable,
- #[cfg(not(target_os = "android"))]
- "$FONT" => Self::Font,
- #[cfg(not(target_os = "android"))]
- "$RUNTIME" => Self::Runtime,
- #[cfg(not(target_os = "android"))]
- "$TEMPLATE" => Self::Template,
- _ => return None,
- };
- Some(res)
- }
- }
- impl<R: Runtime> PathResolver<R> {
- /// Resolves the path with the base directory.
- ///
- /// # Examples
- ///
- /// ```rust,no_run
- /// use tauri::{path::BaseDirectory, Manager};
- /// tauri::Builder::default()
- /// .setup(|app| {
- /// let path = app.path().resolve("path/to/something", BaseDirectory::Config)?;
- /// assert_eq!(path.to_str().unwrap(), "/home/${whoami}/.config/path/to/something");
- /// Ok(())
- /// });
- /// ```
- pub fn resolve<P: AsRef<Path>>(&self, path: P, base_directory: BaseDirectory) -> Result<PathBuf> {
- resolve_path::<R>(self, base_directory, Some(path.as_ref().to_path_buf()))
- }
- /// Parse the given path, resolving a [`BaseDirectory`] variable if the path starts with one.
- ///
- /// # Examples
- ///
- /// ```rust,no_run
- /// use tauri::Manager;
- /// tauri::Builder::default()
- /// .setup(|app| {
- /// let path = app.path().parse("$HOME/.bashrc")?;
- /// assert_eq!(path.to_str().unwrap(), "/home/${whoami}/.bashrc");
- /// Ok(())
- /// });
- /// ```
- pub fn parse<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
- let mut p = PathBuf::new();
- let mut components = path.as_ref().components();
- match components.next() {
- Some(Component::Normal(str)) => {
- if let Some(base_directory) = BaseDirectory::from_variable(&str.to_string_lossy()) {
- p.push(resolve_path::<R>(self, base_directory, None)?);
- } else {
- p.push(str);
- }
- }
- Some(component) => p.push(component),
- None => (),
- }
- for component in components {
- if let Component::ParentDir = component {
- continue;
- }
- p.push(component);
- }
- Ok(p)
- }
- }
- fn resolve_path<R: Runtime>(
- resolver: &PathResolver<R>,
- directory: BaseDirectory,
- path: Option<PathBuf>,
- ) -> Result<PathBuf> {
- let resolve_resource = matches!(directory, BaseDirectory::Resource);
- let mut base_dir_path = match directory {
- BaseDirectory::Audio => resolver.audio_dir(),
- BaseDirectory::Cache => resolver.cache_dir(),
- BaseDirectory::Config => resolver.config_dir(),
- BaseDirectory::Data => resolver.data_dir(),
- BaseDirectory::LocalData => resolver.local_data_dir(),
- BaseDirectory::Document => resolver.document_dir(),
- BaseDirectory::Download => resolver.download_dir(),
- BaseDirectory::Picture => resolver.picture_dir(),
- BaseDirectory::Public => resolver.public_dir(),
- BaseDirectory::Video => resolver.video_dir(),
- BaseDirectory::Resource => resolver.resource_dir(),
- BaseDirectory::Temp => resolver.temp_dir(),
- BaseDirectory::AppConfig => resolver.app_config_dir(),
- BaseDirectory::AppData => resolver.app_data_dir(),
- BaseDirectory::AppLocalData => resolver.app_local_data_dir(),
- BaseDirectory::AppCache => resolver.app_cache_dir(),
- BaseDirectory::AppLog => resolver.app_log_dir(),
- BaseDirectory::Home => resolver.home_dir(),
- #[cfg(not(target_os = "android"))]
- BaseDirectory::Desktop => resolver.desktop_dir(),
- #[cfg(not(target_os = "android"))]
- BaseDirectory::Executable => resolver.executable_dir(),
- #[cfg(not(target_os = "android"))]
- BaseDirectory::Font => resolver.font_dir(),
- #[cfg(not(target_os = "android"))]
- BaseDirectory::Runtime => resolver.runtime_dir(),
- #[cfg(not(target_os = "android"))]
- BaseDirectory::Template => resolver.template_dir(),
- }?;
- if let Some(path) = path {
- // use the same path resolution mechanism as the bundler's resource injection algorithm
- if resolve_resource {
- let mut resource_path = PathBuf::new();
- for component in path.components() {
- match component {
- Component::Prefix(_) => {}
- Component::RootDir => resource_path.push("_root_"),
- Component::CurDir => {}
- Component::ParentDir => resource_path.push("_up_"),
- Component::Normal(p) => resource_path.push(p),
- }
- }
- base_dir_path.push(resource_path);
- } else {
- base_dir_path.push(path);
- }
- }
- Ok(base_dir_path)
- }
- #[cfg(test)]
- mod test {
- use super::SafePathBuf;
- use quickcheck::{Arbitrary, Gen};
- use std::path::PathBuf;
- impl Arbitrary for SafePathBuf {
- fn arbitrary(g: &mut Gen) -> Self {
- Self(PathBuf::arbitrary(g))
- }
- fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
- Box::new(self.0.shrink().map(SafePathBuf))
- }
- }
- }
|