use std::{
collections::{HashMap, HashSet},
fmt,
path::{Path, PathBuf, MAIN_SEPARATOR},
sync::{
atomic::{AtomicU32, Ordering},
Arc, Mutex,
},
};
use tauri_utils::config::FsScope;
use crate::ScopeEventId;
pub use glob::Pattern;
#[derive(Debug, Clone)]
pub enum Event {
PathAllowed(PathBuf),
PathForbidden(PathBuf),
}
type EventListener = Box<dyn Fn(&Event) + Send>;
#[derive(Clone)]
pub struct Scope {
allowed_patterns: Arc<Mutex<HashSet<Pattern>>>,
forbidden_patterns: Arc<Mutex<HashSet<Pattern>>>,
event_listeners: Arc<Mutex<HashMap<ScopeEventId, EventListener>>>,
match_options: glob::MatchOptions,
next_event_id: Arc<AtomicU32>,
}
impl Scope {
fn next_event_id(&self) -> u32 {
self.next_event_id.fetch_add(1, Ordering::Relaxed)
}
}
impl fmt::Debug for Scope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Scope")
.field(
"allowed_patterns",
&self
.allowed_patterns
.lock()
.unwrap()
.iter()
.map(|p| p.as_str())
.collect::<Vec<&str>>(),
)
.field(
"forbidden_patterns",
&self
.forbidden_patterns
.lock()
.unwrap()
.iter()
.map(|p| p.as_str())
.collect::<Vec<&str>>(),
)
.finish()
}
}
fn push_pattern<P: AsRef<Path>, F: Fn(&str) -> Result<Pattern, glob::PatternError>>(
list: &mut HashSet<Pattern>,
pattern: P,
f: F,
) -> crate::Result<()> {
let path: PathBuf = pattern.as_ref().components().collect();
let path_str = path.to_string_lossy();
list.insert(f(&path_str)?);
#[cfg(windows)]
{
use std::path::{Component, Prefix};
let mut components = path.components();
let is_unc = match components.next() {
Some(Component::Prefix(p)) => match p.kind() {
Prefix::VerbatimDisk(..) => true,
_ => false, },
_ => false, };
if is_unc {
let simplified = path
.to_str()
.and_then(|s| s.get(4..))
.map_or(path.as_path(), Path::new);
let simplified_str = simplified.to_string_lossy();
if simplified_str != path_str {
list.insert(f(&simplified_str)?);
}
}
}
if let Some(p) = canonicalize_parent(path) {
list.insert(f(&p.to_string_lossy())?);
}
Ok(())
}
fn canonicalize_parent(mut path: PathBuf) -> Option<PathBuf> {
let mut failed_components = None;
loop {
if let Ok(path) = path.canonicalize() {
break Some(if let Some(p) = failed_components {
path.join(p)
} else {
path
});
}
if let Some(mut last) = path.iter().next_back().map(PathBuf::from) {
if !path.pop() {
break None;
}
if let Some(failed_components) = &failed_components {
last.push(failed_components);
}
failed_components.replace(last);
} else {
break None;
}
}
}
impl Scope {
pub fn new<R: crate::Runtime, M: crate::Manager<R>>(
manager: &M,
scope: &FsScope,
) -> crate::Result<Self> {
let mut allowed_patterns = HashSet::new();
for path in scope.allowed_paths() {
if let Ok(path) = manager.path().parse(path) {
push_pattern(&mut allowed_patterns, path, Pattern::new)?;
}
}
let mut forbidden_patterns = HashSet::new();
if let Some(forbidden_paths) = scope.forbidden_paths() {
for path in forbidden_paths {
if let Ok(path) = manager.path().parse(path) {
push_pattern(&mut forbidden_patterns, path, Pattern::new)?;
}
}
}
let require_literal_leading_dot = match scope {
FsScope::Scope {
require_literal_leading_dot: Some(require),
..
} => *require,
#[cfg(unix)]
_ => true,
#[cfg(windows)]
_ => false,
};
Ok(Self {
allowed_patterns: Arc::new(Mutex::new(allowed_patterns)),
forbidden_patterns: Arc::new(Mutex::new(forbidden_patterns)),
event_listeners: Default::default(),
next_event_id: Default::default(),
match_options: glob::MatchOptions {
require_literal_separator: true,
require_literal_leading_dot,
..Default::default()
},
})
}
pub fn allowed_patterns(&self) -> HashSet<Pattern> {
self.allowed_patterns.lock().unwrap().clone()
}
pub fn forbidden_patterns(&self) -> HashSet<Pattern> {
self.forbidden_patterns.lock().unwrap().clone()
}
pub fn listen<F: Fn(&Event) + Send + 'static>(&self, f: F) -> ScopeEventId {
let id = self.next_event_id();
self.listen_with_id(id, f);
id
}
fn listen_with_id<F: Fn(&Event) + Send + 'static>(&self, id: ScopeEventId, f: F) {
self.event_listeners.lock().unwrap().insert(id, Box::new(f));
}
pub fn once<F: FnOnce(&Event) + Send + 'static>(&self, f: F) -> ScopeEventId {
let listerners = self.event_listeners.clone();
let handler = std::cell::Cell::new(Some(f));
let id = self.next_event_id();
self.listen_with_id(id, move |event| {
listerners.lock().unwrap().remove(&id);
let handler = handler
.take()
.expect("attempted to call handler more than once");
handler(event)
});
id
}
pub fn unlisten(&self, id: ScopeEventId) {
self.event_listeners.lock().unwrap().remove(&id);
}
fn emit(&self, event: Event) {
let listeners = self.event_listeners.lock().unwrap();
let handlers = listeners.values();
for listener in handlers {
listener(&event);
}
}
pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
let path = path.as_ref();
{
let mut list = self.allowed_patterns.lock().unwrap();
push_pattern(&mut list, path, escaped_pattern)?;
push_pattern(&mut list, path, |p| {
escaped_pattern_with(p, if recursive { "**" } else { "*" })
})?;
}
self.emit(Event::PathAllowed(path.to_path_buf()));
Ok(())
}
pub fn allow_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
let path = path.as_ref();
push_pattern(
&mut self.allowed_patterns.lock().unwrap(),
path,
escaped_pattern,
)?;
self.emit(Event::PathAllowed(path.to_path_buf()));
Ok(())
}
pub fn forbid_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
let path = path.as_ref();
{
let mut list = self.forbidden_patterns.lock().unwrap();
push_pattern(&mut list, path, escaped_pattern)?;
push_pattern(&mut list, path, |p| {
escaped_pattern_with(p, if recursive { "**" } else { "*" })
})?;
}
self.emit(Event::PathForbidden(path.to_path_buf()));
Ok(())
}
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,
escaped_pattern,
)?;
self.emit(Event::PathForbidden(path.to_path_buf()));
Ok(())
}
pub fn is_allowed<P: AsRef<Path>>(&self, path: P) -> bool {
let path = try_resolve_symlink_and_canonicalize(path);
if let Ok(path) = path {
let path: PathBuf = path.components().collect();
let forbidden = self
.forbidden_patterns
.lock()
.unwrap()
.iter()
.any(|p| p.matches_path_with(&path, self.match_options));
if forbidden {
false
} else {
let allowed = self
.allowed_patterns
.lock()
.unwrap()
.iter()
.any(|p| p.matches_path_with(&path, self.match_options));
allowed
}
} else {
false
}
}
pub fn is_forbidden<P: AsRef<Path>>(&self, path: P) -> bool {
let path = try_resolve_symlink_and_canonicalize(path);
if let Ok(path) = path {
let path: PathBuf = path.components().collect();
self
.forbidden_patterns
.lock()
.unwrap()
.iter()
.any(|p| p.matches_path_with(&path, self.match_options))
} else {
true
}
}
}
fn try_resolve_symlink_and_canonicalize<P: AsRef<Path>>(path: P) -> crate::Result<PathBuf> {
let path = path.as_ref();
let path = if path.is_symlink() {
std::fs::read_link(path)?
} else {
path.to_path_buf()
};
if !path.exists() {
crate::Result::Ok(path)
} else {
std::fs::canonicalize(path).map_err(Into::into)
}
}
fn escaped_pattern(p: &str) -> Result<Pattern, glob::PatternError> {
Pattern::new(&glob::Pattern::escape(p))
}
fn escaped_pattern_with(p: &str, append: &str) -> Result<Pattern, glob::PatternError> {
if p.ends_with(MAIN_SEPARATOR) {
Pattern::new(&format!("{}{append}", glob::Pattern::escape(p)))
} else {
Pattern::new(&format!(
"{}{}{append}",
glob::Pattern::escape(p),
MAIN_SEPARATOR
))
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use glob::Pattern;
use super::{push_pattern, Scope};
fn new_scope() -> Scope {
Scope {
allowed_patterns: Default::default(),
forbidden_patterns: Default::default(),
event_listeners: Default::default(),
next_event_id: Default::default(),
match_options: glob::MatchOptions {
require_literal_separator: true,
#[cfg(unix)]
require_literal_leading_dot: true,
#[cfg(windows)]
require_literal_leading_dot: false,
..Default::default()
},
}
}
#[test]
fn path_is_escaped() {
let scope = new_scope();
#[cfg(unix)]
{
scope.allow_directory("/home/tauri/**", false).unwrap();
assert!(scope.is_allowed("/home/tauri/**"));
assert!(scope.is_allowed("/home/tauri/**/file"));
assert!(!scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_directory("C:\\home\\tauri\\**", false).unwrap();
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
let scope = new_scope();
#[cfg(unix)]
{
scope.allow_file("/home/tauri/**").unwrap();
assert!(scope.is_allowed("/home/tauri/**"));
assert!(!scope.is_allowed("/home/tauri/**/file"));
assert!(!scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_file("C:\\home\\tauri\\**").unwrap();
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
let scope = new_scope();
#[cfg(unix)]
{
scope.allow_directory("/home/tauri", true).unwrap();
scope.forbid_directory("/home/tauri/**", false).unwrap();
assert!(!scope.is_allowed("/home/tauri/**"));
assert!(!scope.is_allowed("/home/tauri/**/file"));
assert!(scope.is_allowed("/home/tauri/**/inner/file"));
assert!(scope.is_allowed("/home/tauri/inner/folder/anyfile"));
assert!(scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_directory("C:\\home\\tauri", true).unwrap();
scope
.forbid_directory("C:\\home\\tauri\\**", false)
.unwrap();
assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\inner\\folder\\anyfile"));
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
let scope = new_scope();
#[cfg(unix)]
{
scope.allow_directory("/home/tauri", true).unwrap();
scope.forbid_file("/home/tauri/**").unwrap();
assert!(!scope.is_allowed("/home/tauri/**"));
assert!(scope.is_allowed("/home/tauri/**/file"));
assert!(scope.is_allowed("/home/tauri/**/inner/file"));
assert!(scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_directory("C:\\home\\tauri", true).unwrap();
scope.forbid_file("C:\\home\\tauri\\**").unwrap();
assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
let scope = new_scope();
#[cfg(unix)]
{
scope.allow_directory("/home/tauri", false).unwrap();
assert!(scope.is_allowed("/home/tauri/**"));
assert!(!scope.is_allowed("/home/tauri/**/file"));
assert!(!scope.is_allowed("/home/tauri/**/inner/file"));
assert!(scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_directory("C:\\home\\tauri", false).unwrap();
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
}
#[cfg(windows)]
#[test]
fn windows_root_paths() {
let scope = new_scope();
{
scope.allow_directory("\\\\localhost\\c$", true).unwrap();
assert!(scope.is_allowed("\\\\localhost\\c$"));
assert!(scope.is_allowed("\\\\localhost\\c$\\Windows"));
assert!(scope.is_allowed("\\\\localhost\\c$\\NonExistentFile"));
assert!(!scope.is_allowed("\\\\localhost\\d$"));
assert!(!scope.is_allowed("\\\\OtherServer\\Share"));
}
let scope = new_scope();
{
scope
.allow_directory("\\\\?\\UNC\\localhost\\c$", true)
.unwrap();
assert!(scope.is_allowed("\\\\localhost\\c$"));
assert!(scope.is_allowed("\\\\localhost\\c$\\Windows"));
assert!(scope.is_allowed("\\\\?\\UNC\\localhost\\c$\\Windows\\NonExistentFile"));
assert!(!scope.is_allowed("\\\\localhost\\c$\\Windows\\NonExistentFile"));
assert!(!scope.is_allowed("\\\\localhost\\d$"));
assert!(!scope.is_allowed("\\\\OtherServer\\Share"));
}
let scope = new_scope();
{
scope.allow_file("\\\\.\\COM1").unwrap();
assert!(scope.is_allowed("\\\\.\\COM1"));
assert!(!scope.is_allowed("\\\\.\\COM2"));
}
let scope = new_scope();
{
scope.allow_directory("C:\\", true).unwrap();
assert!(scope.is_allowed("C:\\Windows"));
assert!(scope.is_allowed("C:\\Windows\\system.ini"));
assert!(scope.is_allowed("C:\\NonExistentFile"));
assert!(!scope.is_allowed("D:\\home"));
}
let scope = new_scope();
{
scope.allow_directory("\\\\?\\C:\\", true).unwrap();
assert!(scope.is_allowed("C:\\Windows"));
assert!(scope.is_allowed("C:\\Windows\\system.ini"));
assert!(scope.is_allowed("C:\\NonExistentFile"));
assert!(!scope.is_allowed("D:\\home"));
}
let scope = new_scope();
{
scope.allow_file("\\\\?\\anyfile").unwrap();
assert!(scope.is_allowed("\\\\?\\anyfile"));
assert!(!scope.is_allowed("\\\\?\\otherfile"));
}
}
#[test]
fn push_pattern_generated_paths() {
macro_rules! assert_pattern {
($patterns:ident, $pattern:literal) => {
assert!($patterns.contains(&Pattern::new($pattern).unwrap()))
};
}
let mut patterns = HashSet::new();
#[cfg(not(windows))]
{
push_pattern(&mut patterns, "/path/to/dir/", Pattern::new).expect("failed to push pattern");
push_pattern(&mut patterns, "/path/to/dir/**", Pattern::new).expect("failed to push pattern");
assert_pattern!(patterns, "/path/to/dir");
assert_pattern!(patterns, "/path/to/dir/**");
}
#[cfg(windows)]
{
push_pattern(&mut patterns, "C:\\path\\to\\dir", Pattern::new)
.expect("failed to push pattern");
push_pattern(&mut patterns, "C:\\path\\to\\dir\\**", Pattern::new)
.expect("failed to push pattern");
assert_pattern!(patterns, "C:\\path\\to\\dir");
assert_pattern!(patterns, "C:\\path\\to\\dir\\**");
assert_pattern!(patterns, "\\\\?\\C:\\path\\to\\dir");
assert_pattern!(patterns, "\\\\?\\C:\\path\\to\\dir\\**");
}
}
}