diff --git a/.changes/fix-fs-scope-escape-paths.md b/.changes/fix-fs-scope-escape-paths.md index ade71d4a..0430fb0a 100644 --- a/.changes/fix-fs-scope-escape-paths.md +++ b/.changes/fix-fs-scope-escape-paths.md @@ -1,5 +1,6 @@ --- -fs: patch +fs: minor +persisted-scope: minor --- -Paths given to the `Scope` methods are now correctly escaped, preventing issues with paths containing `[]`. +**Breaking Change:** Replaced the custom `tauri_plugin_fs::Scope` struct with `tauri::fs::Scope`. diff --git a/plugins/dialog/src/commands.rs b/plugins/dialog/src/commands.rs index 4129b7b6..a49f7639 100644 --- a/plugins/dialog/src/commands.rs +++ b/plugins/dialog/src/commands.rs @@ -143,7 +143,7 @@ pub(crate) async fn open( for folder in folders { if let Ok(path) = folder.clone().into_path() { if let Some(s) = window.try_fs_scope() { - s.allow_directory(&path, options.recursive); + let _ = s.allow_directory(&path, options.recursive); } tauri_scope.allow_directory(&path, options.directory)?; } @@ -157,7 +157,7 @@ pub(crate) async fn open( if let Some(folder) = &folder { if let Ok(path) = folder.clone().into_path() { if let Some(s) = window.try_fs_scope() { - s.allow_directory(&path, options.recursive); + let _ = s.allow_directory(&path, options.recursive); } tauri_scope.allow_directory(&path, options.directory)?; } @@ -175,7 +175,7 @@ pub(crate) async fn open( for file in files { if let Ok(path) = file.clone().into_path() { if let Some(s) = window.try_fs_scope() { - s.allow_file(&path); + let _ = s.allow_file(&path); } tauri_scope.allow_file(&path)?; @@ -190,7 +190,7 @@ pub(crate) async fn open( if let Some(file) = &file { if let Ok(path) = file.clone().into_path() { if let Some(s) = window.try_fs_scope() { - s.allow_file(&path); + let _ = s.allow_file(&path); } tauri_scope.allow_file(&path)?; } @@ -232,7 +232,7 @@ pub(crate) async fn save( if let Some(p) = &path { if let Ok(path) = p.clone().into_path() { if let Some(s) = window.try_fs_scope() { - s.allow_file(&path); + let _ = s.allow_file(&path); } tauri_scope.allow_file(&path)?; } diff --git a/plugins/fs/build.rs b/plugins/fs/build.rs index 66a12552..cb9d00da 100644 --- a/plugins/fs/build.rs +++ b/plugins/fs/build.rs @@ -7,7 +7,7 @@ use std::{ path::{Path, PathBuf}, }; -#[path = "src/entryraw.rs"] +#[path = "src/scope.rs"] #[allow(dead_code)] mod scope; diff --git a/plugins/fs/src/commands.rs b/plugins/fs/src/commands.rs index efa8891f..e3132781 100644 --- a/plugins/fs/src/commands.rs +++ b/plugins/fs/src/commands.rs @@ -22,7 +22,7 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -use crate::{scope::Entry, Error, FsExt, SafeFilePath}; +use crate::{scope::Entry, Error, SafeFilePath}; #[derive(Debug, thiserror::Error)] pub enum CommandError { @@ -999,6 +999,8 @@ pub fn resolve_path( path }; + let fs_scope = webview.state::(); + let scope = tauri::scope::fs::Scope::new( webview, &FsScope::Scope { @@ -1014,31 +1016,19 @@ pub fn resolve_path( .filter_map(|e| e.path.clone()) .chain(command_scope.denies().iter().filter_map(|e| e.path.clone())) .collect(), - require_literal_leading_dot: webview.fs_scope().require_literal_leading_dot, + require_literal_leading_dot: fs_scope.require_literal_leading_dot, }, )?; - let fs_scope = webview.fs_scope(); - let require_literal_leading_dot = fs_scope.require_literal_leading_dot.unwrap_or(cfg!(unix)); - if fs_scope - .scope - .as_ref() - .map(|s| is_forbidden(s, &path, require_literal_leading_dot)) - .unwrap_or(false) + if is_forbidden(&fs_scope.scope, &path, require_literal_leading_dot) || is_forbidden(&scope, &path, require_literal_leading_dot) { return Err(CommandError::Plugin(Error::PathForbidden(path))); } - if fs_scope - .scope - .as_ref() - .map(|s| s.is_allowed(&path)) - .unwrap_or(false) - || scope.is_allowed(&path) - { + if fs_scope.scope.is_allowed(&path) || scope.is_allowed(&path) { Ok(path) } else { Err(CommandError::Plugin(Error::PathForbidden(path))) diff --git a/plugins/fs/src/entryraw.rs b/plugins/fs/src/entryraw.rs deleted file mode 100644 index 36d195c4..00000000 --- a/plugins/fs/src/entryraw.rs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use std::path::PathBuf; - -use serde::Deserialize; - -#[derive(Deserialize)] -#[serde(untagged)] -pub(crate) enum EntryRaw { - Value(PathBuf), - Object { path: PathBuf }, -} diff --git a/plugins/fs/src/lib.rs b/plugins/fs/src/lib.rs index 126c0389..3240ccb3 100644 --- a/plugins/fs/src/lib.rs +++ b/plugins/fs/src/lib.rs @@ -23,7 +23,6 @@ mod commands; mod config; #[cfg(not(target_os = "android"))] mod desktop; -mod entryraw; mod error; mod file_path; #[cfg(target_os = "android")] @@ -40,7 +39,6 @@ pub use desktop::Fs; pub use mobile::Fs; pub use error::Error; -pub use scope::{Event as ScopeEvent, Scope}; pub use file_path::FilePath; pub use file_path::SafeFilePath; @@ -353,8 +351,8 @@ impl ScopeObject for scope::Entry { raw: Value, ) -> std::result::Result { let path = serde_json::from_value(raw.into()).map(|raw| match raw { - entryraw::EntryRaw::Value(path) => path, - entryraw::EntryRaw::Object { path } => path, + scope::EntryRaw::Value(path) => path, + scope::EntryRaw::Object { path } => path, })?; match app.path().parse(path) { @@ -366,21 +364,26 @@ impl ScopeObject for scope::Entry { } } +pub(crate) struct Scope { + pub(crate) scope: tauri::fs::Scope, + pub(crate) require_literal_leading_dot: Option, +} + pub trait FsExt { - fn fs_scope(&self) -> &Scope; - fn try_fs_scope(&self) -> Option<&Scope>; + fn fs_scope(&self) -> tauri::fs::Scope; + fn try_fs_scope(&self) -> Option; /// Cross platform file system APIs that also support manipulating Android files. fn fs(&self) -> &Fs; } impl> FsExt for T { - fn fs_scope(&self) -> &Scope { - self.state::().inner() + fn fs_scope(&self) -> tauri::fs::Scope { + self.state::().scope.clone() } - fn try_fs_scope(&self) -> Option<&Scope> { - self.try_state::().map(|s| s.inner()) + fn try_fs_scope(&self) -> Option { + self.try_state::().map(|s| s.scope.clone()) } fn fs(&self) -> &Fs { @@ -425,7 +428,7 @@ pub fn init() -> TauriPlugin> { .config() .as_ref() .and_then(|c| c.require_literal_leading_dot), - scope: Some(tauri::fs::Scope::new(app, &FsScope::default())?), + scope: tauri::fs::Scope::new(app, &FsScope::default())?, }; #[cfg(target_os = "android")] @@ -449,9 +452,9 @@ pub fn init() -> TauriPlugin> { let scope = app.fs_scope(); for path in paths { if path.is_file() { - scope.allow_file(path); + let _ = scope.allow_file(path); } else { - scope.allow_directory(path, true); + let _ = scope.allow_directory(path, true); } } } diff --git a/plugins/fs/src/scope.rs b/plugins/fs/src/scope.rs index 44534d1d..7914706a 100644 --- a/plugins/fs/src/scope.rs +++ b/plugins/fs/src/scope.rs @@ -2,126 +2,18 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::{ - collections::HashSet, - path::{Path, PathBuf}, -}; +use std::path::PathBuf; + +use serde::Deserialize; #[derive(Debug)] pub struct Entry { pub path: Option, } -pub type EventId = u32; - -/// Scope change event. -#[derive(Debug, Clone)] -pub enum Event { - /// A path has been allowed. - PathAllowed(PathBuf), - /// A path has been forbidden. - PathForbidden(PathBuf), -} - -#[derive(Default)] -pub struct Scope { - // TODO: Remove Option in v2, just used to keep Default - pub(crate) scope: Option, - pub(crate) require_literal_leading_dot: Option, -} - -impl Scope { - /// Extend the allowed patterns with the given directory. - /// - /// After this function has been called, the frontend will be able to use the Tauri API to read - /// the directory and all of its files. If `recursive` is `true`, subdirectories will be accessible too. - // TODO: Return Result - pub fn allow_directory>(&self, path: P, recursive: bool) { - if let Some(scope) = &self.scope { - let _ = scope.allow_directory(path, recursive); - } - } - - /// Extend the allowed patterns with the given file path. - /// - /// After this function has been called, the frontend will be able to use the Tauri API to read the contents of this file. - // TODO: Return Result - pub fn allow_file>(&self, path: P) { - if let Some(scope) = &self.scope { - let _ = scope.allow_file(path); - } - } - - /// Set the given directory path to be forbidden by this scope. - /// - /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**. - // TODO: Return Result - pub fn forbid_directory>(&self, path: P, recursive: bool) { - if let Some(scope) = &self.scope { - let _ = scope.forbid_directory(path, recursive); - } - } - - /// Set the given file path to be forbidden by this scope. - /// - /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**. - // TODO: Return Result - pub fn forbid_file>(&self, path: P) { - if let Some(scope) = &self.scope { - let _ = scope.forbid_file(path); - } - } - - /// List of allowed paths. - #[deprecated(since = "2.1.0", note = "use `allowed_patterns` instead")] - pub fn allowed(&self) -> Vec { - self.scope - .as_ref() - .map(|s| s.allowed_patterns().clone()) - .unwrap_or_default() - .iter() - .map(|p| PathBuf::from(p.as_str())) - .collect() - } - - /// List of allowed patterns. Note that this does not include paths defined in capabilites. - pub fn allowed_patterns(&self) -> HashSet { - self.scope - .as_ref() - .map(|s| s.allowed_patterns().clone()) - .unwrap_or_default() - } - - /// List of forbidden paths. - #[deprecated(since = "2.1.0", note = "use `forbidden_patterns` instead")] - pub fn forbidden(&self) -> Vec { - self.scope - .as_ref() - .map(|s| s.forbidden_patterns().clone()) - .unwrap_or_default() - .iter() - .map(|p| PathBuf::from(p.as_str())) - .collect() - } - - /// List of forbidden patterns. Note that this does not include paths defined in capabilites. - pub fn forbidden_patterns(&self) -> HashSet { - self.scope - .as_ref() - .map(|s| s.forbidden_patterns()) - .unwrap_or_default() - } - - /// Listen to an event on this scope. - /// Silently fails and returns `0` until v3 if `Scope` was constructed manually instead of getting it via `app.fs_scope()`. - pub fn listen(&self, f: F) -> EventId { - if let Some(scope) = &self.scope { - scope.listen(move |e| match e { - tauri::fs::Event::PathAllowed(p) => f(&Event::PathAllowed(p.to_owned())), - tauri::fs::Event::PathForbidden(p) => f(&Event::PathForbidden(p.to_owned())), - }) - } else { - 0 - } - } +#[derive(Deserialize)] +#[serde(untagged)] +pub(crate) enum EntryRaw { + Value(PathBuf), + Object { path: PathBuf }, } diff --git a/plugins/persisted-scope/src/lib.rs b/plugins/persisted-scope/src/lib.rs index f8dd8ab9..163e449b 100644 --- a/plugins/persisted-scope/src/lib.rs +++ b/plugins/persisted-scope/src/lib.rs @@ -14,13 +14,11 @@ use serde::{Deserialize, Serialize}; use tauri::{ plugin::{Builder, TauriPlugin}, - scope::fs::Pattern as GlobPattern, Manager, Runtime, }; use tauri_plugin_fs::FsExt; use std::{ - collections::HashSet, fs::{create_dir_all, File}, io::Write, path::Path, @@ -44,81 +42,6 @@ const PATTERNS: &[&str] = &[ ]; const REPLACE_WITH: &[&str] = &[r"[", r"]", r"?", r"*", r"\?", r"\\?\", r"\\?\"]; -trait ScopeExt { - type Pattern: ToString; - - fn allow_file(&self, path: &Path); - fn allow_directory(&self, path: &Path, recursive: bool); - - fn forbid_file(&self, path: &Path); - fn forbid_directory(&self, path: &Path, recursive: bool); - - fn allowed_patterns(&self) -> HashSet; - fn forbidden_patterns(&self) -> HashSet; -} - -impl ScopeExt for tauri::scope::fs::Scope { - type Pattern = GlobPattern; - - fn allow_file(&self, path: &Path) { - let _ = tauri::scope::fs::Scope::allow_file(self, path); - } - - fn allow_directory(&self, path: &Path, recursive: bool) { - let _ = tauri::scope::fs::Scope::allow_directory(self, path, recursive); - } - - fn forbid_file(&self, path: &Path) { - let _ = tauri::scope::fs::Scope::forbid_file(self, path); - } - - fn forbid_directory(&self, path: &Path, recursive: bool) { - let _ = tauri::scope::fs::Scope::forbid_directory(self, path, recursive); - } - - fn allowed_patterns(&self) -> HashSet { - tauri::scope::fs::Scope::allowed_patterns(self) - } - - fn forbidden_patterns(&self) -> HashSet { - tauri::scope::fs::Scope::forbidden_patterns(self) - } -} - -impl ScopeExt for tauri_plugin_fs::Scope { - type Pattern = String; - - fn allow_file(&self, path: &Path) { - tauri_plugin_fs::Scope::allow_file(self, path); - } - - fn allow_directory(&self, path: &Path, recursive: bool) { - tauri_plugin_fs::Scope::allow_directory(self, path, recursive); - } - - fn forbid_file(&self, path: &Path) { - tauri_plugin_fs::Scope::forbid_file(self, path); - } - - fn forbid_directory(&self, path: &Path, recursive: bool) { - tauri_plugin_fs::Scope::forbid_directory(self, path, recursive); - } - - fn allowed_patterns(&self) -> HashSet { - self.allowed() - .into_iter() - .map(|p| p.to_string_lossy().to_string()) - .collect() - } - - fn forbidden_patterns(&self) -> HashSet { - self.forbidden() - .into_iter() - .map(|p| p.to_string_lossy().to_string()) - .collect() - } -} - #[derive(Debug, thiserror::Error)] enum Error { #[error(transparent)] @@ -179,41 +102,41 @@ fn fix_directory(path_str: &str) -> &Path { path } -fn allow_path(scope: &impl ScopeExt, path: &str) { +fn allow_path(scope: &tauri::fs::Scope, path: &str) { let target_type = detect_scope_type(path); match target_type { TargetType::File => { - scope.allow_file(Path::new(path)); + let _ = scope.allow_file(Path::new(path)); } TargetType::Directory => { // We remove the '*' at the end of it, else it will be escaped by the pattern. - scope.allow_directory(fix_directory(path), false); + let _ = scope.allow_directory(fix_directory(path), false); } TargetType::RecursiveDirectory => { // We remove the '**' at the end of it, else it will be escaped by the pattern. - scope.allow_directory(fix_directory(path), true); + let _ = scope.allow_directory(fix_directory(path), true); } } } -fn forbid_path(scope: &impl ScopeExt, path: &str) { +fn forbid_path(scope: &tauri::fs::Scope, path: &str) { let target_type = detect_scope_type(path); match target_type { TargetType::File => { - scope.forbid_file(Path::new(path)); + let _ = scope.forbid_file(Path::new(path)); } TargetType::Directory => { - scope.forbid_directory(fix_directory(path), false); + let _ = scope.forbid_directory(fix_directory(path), false); } TargetType::RecursiveDirectory => { - scope.forbid_directory(fix_directory(path), true); + let _ = scope.forbid_directory(fix_directory(path), true); } } } -fn save_scopes(scope: &impl ScopeExt, app_dir: &Path, scope_state_path: &Path) { +fn save_scopes(scope: &tauri::fs::Scope, app_dir: &Path, scope_state_path: &Path) { let scope = Scope { allowed_paths: scope .allowed_patterns() @@ -250,8 +173,11 @@ pub fn init() -> TauriPlugin { #[cfg(feature = "protocol-asset")] let asset_scope_state_path = app_dir.join(ASSET_SCOPE_STATE_FILENAME); - if let Some(fs_scope) = fs_scope { - fs_scope.forbid_file(&fs_scope_state_path); + if let Some(fs_scope) = &fs_scope { + let _ = fs_scope.forbid_file(&fs_scope_state_path); + } else { + #[cfg(debug_assertions)] + eprintln!("Please make sure to register the `fs` plugin before the `persisted-scope` plugin!"); } #[cfg(feature = "protocol-asset")] let _ = asset_protocol_scope.forbid_file(&asset_scope_state_path); @@ -260,7 +186,7 @@ pub fn init() -> TauriPlugin { // We will still save some semi-broken values because the scope events are quite spammy and we don't want to reduce runtime performance any further. let ac = AhoCorasick::new(PATTERNS).unwrap(/* This should be impossible to fail since we're using a small static input */); - if let Some(fs_scope) = fs_scope { + if let Some(fs_scope) = &fs_scope { if fs_scope_state_path.exists() { let scope: Scope = std::fs::read(&fs_scope_state_path) .map_err(Error::from) @@ -269,7 +195,7 @@ pub fn init() -> TauriPlugin { for allowed in &scope.allowed_paths { let allowed = fix_pattern(&ac, allowed); - allow_path(fs_scope, &allowed); + allow_path(fs_scope, &allowed); } for forbidden in &scope.forbidden_patterns { let forbidden = fix_pattern(&ac, forbidden); @@ -305,11 +231,11 @@ pub fn init() -> TauriPlugin { #[cfg(feature = "protocol-asset")] let app_dir_ = app_dir.clone(); - if let Some(fs_scope) = fs_scope { + if let Some(fs_scope) = &fs_scope { let app_ = app.clone(); fs_scope.listen(move |event| { - if let tauri_plugin_fs::ScopeEvent::PathAllowed(_) = event { - save_scopes(app_.fs_scope(), &app_dir, &fs_scope_state_path); + if let tauri::fs::Event::PathAllowed(_) = event { + save_scopes(&app_.fs_scope(), &app_dir, &fs_scope_state_path); } }); }