Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): filesystem and asset protocol scope events #3609

Merged
merged 1 commit into from
Mar 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/fs-scope-events.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -1161,14 +1161,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -832,9 +832,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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