Skip to content

Commit

Permalink
feat(core): filesystem and asset protocol scope events (#3609)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog committed Mar 5, 2022
1 parent 3fe0260 commit 58070c1
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 52 deletions.
5 changes: 5 additions & 0 deletions .changes/fs-scope-events.md
@@ -0,0 +1,5 @@
---
"tauri": patch
---

Allow listening to events on the filesystem and asset scopes.
4 changes: 2 additions & 2 deletions core/tauri/src/app.rs
Expand Up @@ -1167,14 +1167,14 @@ impl<R: Runtime> Builder<R> {
app.package_info(),
&env,
&app.config().tauri.allowlist.fs.scope,
),
)?,
#[cfg(protocol_asset)]
asset_protocol: FsScope::for_fs_api(
&app.manager.config(),
app.package_info(),
&env,
&app.config().tauri.allowlist.protocol.asset_scope,
),
)?,
#[cfg(http_request)]
http: crate::scope::HttpScope::for_http_api(&app.config().tauri.allowlist.http.scope),
#[cfg(shell_scope)]
Expand Down
10 changes: 6 additions & 4 deletions core/tauri/src/endpoints/dialog.rs
Expand Up @@ -106,21 +106,23 @@ impl Cmd {
let res = if options.directory {
let folder = dialog_builder.pick_folder();
if let Some(path) = &folder {
scopes.allow_directory(path, options.recursive);
scopes
.allow_directory(path, options.recursive)
.map_err(crate::error::into_anyhow)?;
}
folder.into()
} else if options.multiple {
let files = dialog_builder.pick_files();
if let Some(files) = &files {
for file in files {
scopes.allow_file(file);
scopes.allow_file(file).map_err(crate::error::into_anyhow)?;
}
}
files.into()
} else {
let file = dialog_builder.pick_file();
if let Some(file) = &file {
scopes.allow_file(file);
scopes.allow_file(file).map_err(crate::error::into_anyhow)?;
}
file.into()
};
Expand Down Expand Up @@ -151,7 +153,7 @@ impl Cmd {

let path = dialog_builder.save_file();
if let Some(p) = &path {
scopes.allow_file(p);
scopes.allow_file(p).map_err(crate::error::into_anyhow)?;
}

Ok(path)
Expand Down
3 changes: 3 additions & 0 deletions core/tauri/src/error.rs
Expand Up @@ -107,6 +107,9 @@ pub enum Error {
/// An invalid window URL was provided. Includes details about the error.
#[error("invalid window url: {0}")]
InvalidWindowUrl(&'static str),
/// Invalid glob pattern.
#[error("invalid glob pattern: {0}")]
GlobPattern(#[from] glob::PatternError),
}

pub(crate) fn into_anyhow<T: std::fmt::Display>(err: T) -> anyhow::Error {
Expand Down
4 changes: 2 additions & 2 deletions core/tauri/src/manager.rs
Expand Up @@ -818,9 +818,9 @@ impl<R: Runtime> WindowManager<R> {
let scopes = window.state::<Scopes>();
for path in &paths {
if path.is_file() {
scopes.allow_file(path);
let _ = scopes.allow_file(path);
} else {
scopes.allow_directory(path, false);
let _ = scopes.allow_directory(path, false);
}
}
window.emit_and_trigger("tauri://file-drop", paths)
Expand Down
122 changes: 87 additions & 35 deletions core/tauri/src/scope/fs.rs
Expand Up @@ -3,33 +3,47 @@
// SPDX-License-Identifier: MIT

use std::{
collections::{HashMap, HashSet},
fmt,
path::{Path, PathBuf},
sync::{Arc, Mutex},
};

use glob::Pattern;
pub use glob::Pattern;
use tauri_utils::{
config::{Config, FsAllowlistScope},
Env, PackageInfo,
};
use uuid::Uuid;

use crate::api::path::parse as parse_path;

/// Scope change event.
#[derive(Debug, Clone)]
pub enum Event {
/// A path has been allowed.
PathAllowed(PathBuf),
/// A path has been forbidden.
PathForbidden(PathBuf),
}

type EventListener = Box<dyn Fn(&Event) + Send>;

/// Scope for filesystem access.
#[derive(Clone)]
pub struct Scope {
allow_patterns: Arc<Mutex<Vec<Pattern>>>,
forbidden_patterns: Arc<Mutex<Vec<Pattern>>>,
alllowed_patterns: Arc<Mutex<HashSet<Pattern>>>,
forbidden_patterns: Arc<Mutex<HashSet<Pattern>>>,
event_listeners: Arc<Mutex<HashMap<Uuid, EventListener>>>,
}

impl fmt::Debug for Scope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Scope")
.field(
"allow_patterns",
"alllowed_patterns",
&self
.allow_patterns
.alllowed_patterns
.lock()
.unwrap()
.iter()
Expand All @@ -50,85 +64,123 @@ impl fmt::Debug for Scope {
}
}

fn push_pattern<P: AsRef<Path>>(list: &mut Vec<Pattern>, pattern: P) {
let pattern: PathBuf = pattern.as_ref().components().collect();
list.push(Pattern::new(&pattern.to_string_lossy()).expect("invalid glob pattern"));
fn push_pattern<P: AsRef<Path>>(list: &mut HashSet<Pattern>, pattern: P) -> crate::Result<()> {
let path: PathBuf = pattern.as_ref().components().collect();
list.insert(Pattern::new(&path.to_string_lossy())?);
#[cfg(windows)]
{
list
.push(Pattern::new(&format!("\\\\?\\{}", pattern.display())).expect("invalid glob pattern"));
list.insert(Pattern::new(&format!("\\\\?\\{}", path.display()))?);
}
Ok(())
}

impl Scope {
/// Creates a new scope from a `FsAllowlistScope` configuration.
pub fn for_fs_api(
pub(crate) fn for_fs_api(
config: &Config,
package_info: &PackageInfo,
env: &Env,
scope: &FsAllowlistScope,
) -> Self {
let mut allow_patterns = Vec::new();
) -> crate::Result<Self> {
let mut alllowed_patterns = HashSet::new();
for path in scope.allowed_paths() {
if let Ok(path) = parse_path(config, package_info, env, path) {
push_pattern(&mut allow_patterns, path);
push_pattern(&mut alllowed_patterns, path)?;
}
}

let mut forbidden_patterns = Vec::new();
let mut forbidden_patterns = HashSet::new();
if let Some(forbidden_paths) = scope.forbidden_paths() {
for path in forbidden_paths {
if let Ok(path) = parse_path(config, package_info, env, path) {
push_pattern(&mut forbidden_patterns, path);
push_pattern(&mut forbidden_patterns, path)?;
}
}
}

Self {
allow_patterns: Arc::new(Mutex::new(allow_patterns)),
Ok(Self {
alllowed_patterns: Arc::new(Mutex::new(alllowed_patterns)),
forbidden_patterns: Arc::new(Mutex::new(forbidden_patterns)),
event_listeners: Default::default(),
})
}

/// The list of allowed patterns.
pub fn allowed_patterns(&self) -> HashSet<Pattern> {
self.alllowed_patterns.lock().unwrap().clone()
}

/// The list of forbidden patterns.
pub fn forbidden_patterns(&self) -> HashSet<Pattern> {
self.forbidden_patterns.lock().unwrap().clone()
}

/// Listen to an event on this scope.
pub fn listen<F: Fn(&Event) + Send + 'static>(&self, f: F) -> Uuid {
let id = Uuid::new_v4();
self.event_listeners.lock().unwrap().insert(id, Box::new(f));
id
}

fn trigger(&self, event: Event) {
for listener in self.event_listeners.lock().unwrap().values() {
listener(&event);
}
}

/// 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 and subdirectories.
pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) {
pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
let path = path.as_ref().to_path_buf();
let mut list = self.allow_patterns.lock().unwrap();
{
let mut list = self.alllowed_patterns.lock().unwrap();

// allow the directory to be read
push_pattern(&mut list, &path);
// allow its files and subdirectories to be read
push_pattern(&mut list, path.join(if recursive { "**" } else { "*" }));
// allow the directory to be read
push_pattern(&mut list, &path)?;
// allow its files and subdirectories to be read
push_pattern(&mut list, path.join(if recursive { "**" } else { "*" }))?;
}
self.trigger(Event::PathAllowed(path));
Ok(())
}

/// 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.
pub fn allow_file<P: AsRef<Path>>(&self, path: P) {
push_pattern(&mut self.allow_patterns.lock().unwrap(), path);
pub fn allow_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
let path = path.as_ref();
push_pattern(&mut self.alllowed_patterns.lock().unwrap(), &path)?;
self.trigger(Event::PathAllowed(path.to_path_buf()));
Ok(())
}

/// Set the given directory path to be forbidden by this scope.
///
/// **Note:** this takes precedence over allowed paths, so its access gets denied **always**.
pub fn forbid_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) {
pub fn forbid_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
let path = path.as_ref().to_path_buf();
let mut list = self.forbidden_patterns.lock().unwrap();
{
let mut list = self.forbidden_patterns.lock().unwrap();

// allow the directory to be read
push_pattern(&mut list, &path);
// allow its files and subdirectories to be read
push_pattern(&mut list, path.join(if recursive { "**" } else { "*" }));
// allow the directory to be read
push_pattern(&mut list, &path)?;
// allow its files and subdirectories to be read
push_pattern(&mut list, path.join(if recursive { "**" } else { "*" }))?;
}
self.trigger(Event::PathForbidden(path));
Ok(())
}

/// Set the given file path to be forbidden by this scope.
///
/// **Note:** this takes precedence over allowed paths, so its access gets denied **always**.
pub fn forbid_file<P: AsRef<Path>>(&self, path: P) {
push_pattern(&mut self.forbidden_patterns.lock().unwrap(), path);
pub fn forbid_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
let path = path.as_ref();
push_pattern(&mut self.forbidden_patterns.lock().unwrap(), &path)?;
self.trigger(Event::PathForbidden(path.to_path_buf()));
Ok(())
}

/// Determines if the given path is allowed on this scope.
Expand All @@ -154,7 +206,7 @@ impl Scope {
false
} else {
let allowed = self
.allow_patterns
.alllowed_patterns
.lock()
.unwrap()
.iter()
Expand Down
3 changes: 2 additions & 1 deletion core/tauri/src/scope/http.rs
Expand Up @@ -13,7 +13,8 @@ pub struct Scope {

impl Scope {
/// Creates a new scope from the allowlist's `http` scope configuration.
pub fn for_http_api(scope: &HttpAllowlistScope) -> Self {
#[allow(dead_code)]
pub(crate) fn for_http_api(scope: &HttpAllowlistScope) -> Self {
Self {
allowed_urls: scope
.0
Expand Down
16 changes: 9 additions & 7 deletions core/tauri/src/scope/mod.rs
Expand Up @@ -8,7 +8,7 @@ mod http;
mod shell;

pub use self::http::Scope as HttpScope;
pub use fs::Scope as FsScope;
pub use fs::{Event as FsScopeEvent, Pattern as GlobPattern, Scope as FsScope};
#[cfg(shell_scope)]
pub use shell::{
ExecuteArgs, Scope as ShellScope, ScopeAllowedArg as ShellScopeAllowedArg,
Expand All @@ -29,16 +29,18 @@ pub(crate) struct Scopes {

impl Scopes {
#[allow(dead_code)]
pub(crate) fn allow_directory(&self, path: &Path, recursive: bool) {
self.fs.allow_directory(path, recursive);
pub(crate) fn allow_directory(&self, path: &Path, recursive: bool) -> crate::Result<()> {
self.fs.allow_directory(path, recursive)?;
#[cfg(protocol_asset)]
self.asset_protocol.allow_directory(path, recursive);
self.asset_protocol.allow_directory(path, recursive)?;
Ok(())
}

#[allow(dead_code)]
pub(crate) fn allow_file(&self, path: &Path) {
self.fs.allow_file(path);
pub(crate) fn allow_file(&self, path: &Path) -> crate::Result<()> {
self.fs.allow_file(path)?;
#[cfg(protocol_asset)]
self.asset_protocol.allow_file(path);
self.asset_protocol.allow_file(path)?;
Ok(())
}
}
2 changes: 1 addition & 1 deletion core/tauri/src/scope/shell.rs
Expand Up @@ -193,7 +193,7 @@ pub enum ScopeError {

impl Scope {
/// Creates a new shell scope.
pub fn new(scope: ScopeConfig) -> Self {
pub(crate) fn new(scope: ScopeConfig) -> Self {
Self(scope)
}

Expand Down

0 comments on commit 58070c1

Please sign in to comment.