1use std::{
6 collections::{HashMap, HashSet},
7 fmt,
8 path::{Path, PathBuf, MAIN_SEPARATOR},
9 sync::{
10 atomic::{AtomicU32, Ordering},
11 Arc, Mutex,
12 },
13};
14
15use tauri_utils::config::FsScope;
16
17use crate::ScopeEventId;
18
19pub use glob::Pattern;
20
21#[derive(Debug, Clone)]
23pub enum Event {
24 PathAllowed(PathBuf),
26 PathForbidden(PathBuf),
28}
29
30type EventListener = Box<dyn Fn(&Event) + Send>;
31
32#[derive(Clone)]
34pub struct Scope {
35 allowed_patterns: Arc<Mutex<HashSet<Pattern>>>,
36 forbidden_patterns: Arc<Mutex<HashSet<Pattern>>>,
37 event_listeners: Arc<Mutex<HashMap<ScopeEventId, EventListener>>>,
38 match_options: glob::MatchOptions,
39 next_event_id: Arc<AtomicU32>,
40}
41
42impl Scope {
43 fn next_event_id(&self) -> u32 {
44 self.next_event_id.fetch_add(1, Ordering::Relaxed)
45 }
46}
47
48impl fmt::Debug for Scope {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 f.debug_struct("Scope")
51 .field(
52 "allowed_patterns",
53 &self
54 .allowed_patterns
55 .lock()
56 .unwrap()
57 .iter()
58 .map(|p| p.as_str())
59 .collect::<Vec<&str>>(),
60 )
61 .field(
62 "forbidden_patterns",
63 &self
64 .forbidden_patterns
65 .lock()
66 .unwrap()
67 .iter()
68 .map(|p| p.as_str())
69 .collect::<Vec<&str>>(),
70 )
71 .finish()
72 }
73}
74
75fn push_pattern<P: AsRef<Path>, F: Fn(&str) -> Result<Pattern, glob::PatternError>>(
76 list: &mut HashSet<Pattern>,
77 pattern: P,
78 f: F,
79) -> crate::Result<()> {
80 let path: PathBuf = pattern.as_ref().components().collect();
84
85 let path_str = path.to_string_lossy();
87 list.insert(f(&path_str)?);
88
89 #[cfg(windows)]
94 {
95 use std::path::{Component, Prefix};
96
97 let mut components = path.components();
98
99 let is_unc = match components.next() {
100 Some(Component::Prefix(p)) => match p.kind() {
101 Prefix::VerbatimDisk(..) => true,
102 _ => false, },
104 _ => false, };
106
107 if is_unc {
108 let simplified = path
112 .to_str()
113 .and_then(|s| s.get(4..))
114 .map_or(path.as_path(), Path::new);
115
116 let simplified_str = simplified.to_string_lossy();
117 if simplified_str != path_str {
118 list.insert(f(&simplified_str)?);
119 }
120 }
121 }
122
123 if let Some(p) = canonicalize_parent(path) {
127 list.insert(f(&p.to_string_lossy())?);
128 }
129
130 Ok(())
131}
132
133fn canonicalize_parent(mut path: PathBuf) -> Option<PathBuf> {
146 let mut failed_components = None;
147
148 loop {
149 if let Ok(path) = path.canonicalize() {
150 break Some(if let Some(p) = failed_components {
151 path.join(p)
152 } else {
153 path
154 });
155 }
156
157 if let Some(mut last) = path.iter().next_back().map(PathBuf::from) {
159 if !path.pop() {
162 break None;
163 }
164
165 if let Some(failed_components) = &failed_components {
168 last.push(failed_components);
169 }
170 failed_components.replace(last);
171 } else {
172 break None;
173 }
174 }
175}
176impl Scope {
177 pub fn new<R: crate::Runtime, M: crate::Manager<R>>(
179 manager: &M,
180 scope: &FsScope,
181 ) -> crate::Result<Self> {
182 let mut allowed_patterns = HashSet::new();
183 for path in scope.allowed_paths() {
184 if let Ok(path) = manager.path().parse(path) {
185 push_pattern(&mut allowed_patterns, path, Pattern::new)?;
186 }
187 }
188
189 let mut forbidden_patterns = HashSet::new();
190 if let Some(forbidden_paths) = scope.forbidden_paths() {
191 for path in forbidden_paths {
192 if let Ok(path) = manager.path().parse(path) {
193 push_pattern(&mut forbidden_patterns, path, Pattern::new)?;
194 }
195 }
196 }
197
198 let require_literal_leading_dot = match scope {
199 FsScope::Scope {
200 require_literal_leading_dot: Some(require),
201 ..
202 } => *require,
203 #[cfg(unix)]
205 _ => true,
206 #[cfg(windows)]
207 _ => false,
208 };
209
210 Ok(Self {
211 allowed_patterns: Arc::new(Mutex::new(allowed_patterns)),
212 forbidden_patterns: Arc::new(Mutex::new(forbidden_patterns)),
213 event_listeners: Default::default(),
214 next_event_id: Default::default(),
215 match_options: glob::MatchOptions {
216 require_literal_separator: true,
219 require_literal_leading_dot,
220 ..Default::default()
221 },
222 })
223 }
224
225 pub fn allowed_patterns(&self) -> HashSet<Pattern> {
227 self.allowed_patterns.lock().unwrap().clone()
228 }
229
230 pub fn forbidden_patterns(&self) -> HashSet<Pattern> {
232 self.forbidden_patterns.lock().unwrap().clone()
233 }
234
235 pub fn listen<F: Fn(&Event) + Send + 'static>(&self, f: F) -> ScopeEventId {
237 let id = self.next_event_id();
238 self.listen_with_id(id, f);
239 id
240 }
241
242 fn listen_with_id<F: Fn(&Event) + Send + 'static>(&self, id: ScopeEventId, f: F) {
243 self.event_listeners.lock().unwrap().insert(id, Box::new(f));
244 }
245
246 pub fn once<F: FnOnce(&Event) + Send + 'static>(&self, f: F) -> ScopeEventId {
248 let listerners = self.event_listeners.clone();
249 let handler = std::cell::Cell::new(Some(f));
250 let id = self.next_event_id();
251 self.listen_with_id(id, move |event| {
252 listerners.lock().unwrap().remove(&id);
253 let handler = handler
254 .take()
255 .expect("attempted to call handler more than once");
256 handler(event)
257 });
258 id
259 }
260
261 pub fn unlisten(&self, id: ScopeEventId) {
263 self.event_listeners.lock().unwrap().remove(&id);
264 }
265
266 fn emit(&self, event: Event) {
267 let listeners = self.event_listeners.lock().unwrap();
268 let handlers = listeners.values();
269 for listener in handlers {
270 listener(&event);
271 }
272 }
273
274 pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
279 let path = path.as_ref();
280 {
281 let mut list = self.allowed_patterns.lock().unwrap();
282
283 push_pattern(&mut list, path, escaped_pattern)?;
285 push_pattern(&mut list, path, |p| {
287 escaped_pattern_with(p, if recursive { "**" } else { "*" })
288 })?;
289 }
290 self.emit(Event::PathAllowed(path.to_path_buf()));
291 Ok(())
292 }
293
294 pub fn allow_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
298 let path = path.as_ref();
299 push_pattern(
300 &mut self.allowed_patterns.lock().unwrap(),
301 path,
302 escaped_pattern,
303 )?;
304 self.emit(Event::PathAllowed(path.to_path_buf()));
305 Ok(())
306 }
307
308 pub fn forbid_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
312 let path = path.as_ref();
313 {
314 let mut list = self.forbidden_patterns.lock().unwrap();
315
316 push_pattern(&mut list, path, escaped_pattern)?;
318 push_pattern(&mut list, path, |p| {
320 escaped_pattern_with(p, if recursive { "**" } else { "*" })
321 })?;
322 }
323 self.emit(Event::PathForbidden(path.to_path_buf()));
324 Ok(())
325 }
326
327 pub fn forbid_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
331 let path = path.as_ref();
332 push_pattern(
333 &mut self.forbidden_patterns.lock().unwrap(),
334 path,
335 escaped_pattern,
336 )?;
337 self.emit(Event::PathForbidden(path.to_path_buf()));
338 Ok(())
339 }
340
341 pub fn is_allowed<P: AsRef<Path>>(&self, path: P) -> bool {
347 let path = try_resolve_symlink_and_canonicalize(path);
348
349 if let Ok(path) = path {
350 let path: PathBuf = path.components().collect();
351 let forbidden = self
352 .forbidden_patterns
353 .lock()
354 .unwrap()
355 .iter()
356 .any(|p| p.matches_path_with(&path, self.match_options));
357
358 if forbidden {
359 false
360 } else {
361 let allowed = self
362 .allowed_patterns
363 .lock()
364 .unwrap()
365 .iter()
366 .any(|p| p.matches_path_with(&path, self.match_options));
367
368 allowed
369 }
370 } else {
371 false
372 }
373 }
374
375 pub fn is_forbidden<P: AsRef<Path>>(&self, path: P) -> bool {
379 let path = try_resolve_symlink_and_canonicalize(path);
380
381 if let Ok(path) = path {
382 let path: PathBuf = path.components().collect();
383 self
384 .forbidden_patterns
385 .lock()
386 .unwrap()
387 .iter()
388 .any(|p| p.matches_path_with(&path, self.match_options))
389 } else {
390 true
391 }
392 }
393}
394
395fn try_resolve_symlink_and_canonicalize<P: AsRef<Path>>(path: P) -> crate::Result<PathBuf> {
396 let path = path.as_ref();
397 let path = if path.is_symlink() {
398 std::fs::read_link(path)?
399 } else {
400 path.to_path_buf()
401 };
402 if !path.exists() {
403 crate::Result::Ok(path)
404 } else {
405 std::fs::canonicalize(path).map_err(Into::into)
406 }
407}
408
409fn escaped_pattern(p: &str) -> Result<Pattern, glob::PatternError> {
410 Pattern::new(&glob::Pattern::escape(p))
411}
412
413fn escaped_pattern_with(p: &str, append: &str) -> Result<Pattern, glob::PatternError> {
414 if p.ends_with(MAIN_SEPARATOR) {
415 Pattern::new(&format!("{}{append}", glob::Pattern::escape(p)))
416 } else {
417 Pattern::new(&format!(
418 "{}{}{append}",
419 glob::Pattern::escape(p),
420 MAIN_SEPARATOR
421 ))
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use std::collections::HashSet;
428
429 use glob::Pattern;
430
431 use super::{push_pattern, Scope};
432
433 fn new_scope() -> Scope {
434 Scope {
435 allowed_patterns: Default::default(),
436 forbidden_patterns: Default::default(),
437 event_listeners: Default::default(),
438 next_event_id: Default::default(),
439 match_options: glob::MatchOptions {
440 require_literal_separator: true,
443 #[cfg(unix)]
445 require_literal_leading_dot: true,
446 #[cfg(windows)]
447 require_literal_leading_dot: false,
448 ..Default::default()
449 },
450 }
451 }
452
453 #[test]
454 fn path_is_escaped() {
455 let scope = new_scope();
456 #[cfg(unix)]
457 {
458 scope.allow_directory("/home/tauri/**", false).unwrap();
459 assert!(scope.is_allowed("/home/tauri/**"));
460 assert!(scope.is_allowed("/home/tauri/**/file"));
461 assert!(!scope.is_allowed("/home/tauri/anyfile"));
462 }
463 #[cfg(windows)]
464 {
465 scope.allow_directory("C:\\home\\tauri\\**", false).unwrap();
466 assert!(scope.is_allowed("C:\\home\\tauri\\**"));
467 assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
468 assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
469 }
470
471 let scope = new_scope();
472 #[cfg(unix)]
473 {
474 scope.allow_file("/home/tauri/**").unwrap();
475 assert!(scope.is_allowed("/home/tauri/**"));
476 assert!(!scope.is_allowed("/home/tauri/**/file"));
477 assert!(!scope.is_allowed("/home/tauri/anyfile"));
478 }
479 #[cfg(windows)]
480 {
481 scope.allow_file("C:\\home\\tauri\\**").unwrap();
482 assert!(scope.is_allowed("C:\\home\\tauri\\**"));
483 assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
484 assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
485 }
486
487 let scope = new_scope();
488 #[cfg(unix)]
489 {
490 scope.allow_directory("/home/tauri", true).unwrap();
491 scope.forbid_directory("/home/tauri/**", false).unwrap();
492 assert!(!scope.is_allowed("/home/tauri/**"));
493 assert!(!scope.is_allowed("/home/tauri/**/file"));
494 assert!(scope.is_allowed("/home/tauri/**/inner/file"));
495 assert!(scope.is_allowed("/home/tauri/inner/folder/anyfile"));
496 assert!(scope.is_allowed("/home/tauri/anyfile"));
497 }
498 #[cfg(windows)]
499 {
500 scope.allow_directory("C:\\home\\tauri", true).unwrap();
501 scope
502 .forbid_directory("C:\\home\\tauri\\**", false)
503 .unwrap();
504 assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
505 assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
506 assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
507 assert!(scope.is_allowed("C:\\home\\tauri\\inner\\folder\\anyfile"));
508 assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
509 }
510
511 let scope = new_scope();
512 #[cfg(unix)]
513 {
514 scope.allow_directory("/home/tauri", true).unwrap();
515 scope.forbid_file("/home/tauri/**").unwrap();
516 assert!(!scope.is_allowed("/home/tauri/**"));
517 assert!(scope.is_allowed("/home/tauri/**/file"));
518 assert!(scope.is_allowed("/home/tauri/**/inner/file"));
519 assert!(scope.is_allowed("/home/tauri/anyfile"));
520 }
521 #[cfg(windows)]
522 {
523 scope.allow_directory("C:\\home\\tauri", true).unwrap();
524 scope.forbid_file("C:\\home\\tauri\\**").unwrap();
525 assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
526 assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
527 assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
528 assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
529 }
530
531 let scope = new_scope();
532 #[cfg(unix)]
533 {
534 scope.allow_directory("/home/tauri", false).unwrap();
535 assert!(scope.is_allowed("/home/tauri/**"));
536 assert!(!scope.is_allowed("/home/tauri/**/file"));
537 assert!(!scope.is_allowed("/home/tauri/**/inner/file"));
538 assert!(scope.is_allowed("/home/tauri/anyfile"));
539 }
540 #[cfg(windows)]
541 {
542 scope.allow_directory("C:\\home\\tauri", false).unwrap();
543 assert!(scope.is_allowed("C:\\home\\tauri\\**"));
544 assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
545 assert!(!scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
546 assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
547 }
548 }
549
550 #[cfg(windows)]
551 #[test]
552 fn windows_root_paths() {
553 let scope = new_scope();
554 {
555 scope.allow_directory("\\\\localhost\\c$", true).unwrap();
557 assert!(scope.is_allowed("\\\\localhost\\c$"));
558 assert!(scope.is_allowed("\\\\localhost\\c$\\Windows"));
559 assert!(scope.is_allowed("\\\\localhost\\c$\\NonExistentFile"));
560 assert!(!scope.is_allowed("\\\\localhost\\d$"));
561 assert!(!scope.is_allowed("\\\\OtherServer\\Share"));
562 }
563
564 let scope = new_scope();
565 {
566 scope
568 .allow_directory("\\\\?\\UNC\\localhost\\c$", true)
569 .unwrap();
570 assert!(scope.is_allowed("\\\\localhost\\c$"));
571 assert!(scope.is_allowed("\\\\localhost\\c$\\Windows"));
572 assert!(scope.is_allowed("\\\\?\\UNC\\localhost\\c$\\Windows\\NonExistentFile"));
573 assert!(!scope.is_allowed("\\\\localhost\\c$\\Windows\\NonExistentFile"));
575 assert!(!scope.is_allowed("\\\\localhost\\d$"));
576 assert!(!scope.is_allowed("\\\\OtherServer\\Share"));
577 }
578
579 let scope = new_scope();
580 {
581 scope.allow_file("\\\\.\\COM1").unwrap();
583 assert!(scope.is_allowed("\\\\.\\COM1"));
584 assert!(!scope.is_allowed("\\\\.\\COM2"));
585 }
586
587 let scope = new_scope();
588 {
589 scope.allow_directory("C:\\", true).unwrap();
591 assert!(scope.is_allowed("C:\\Windows"));
592 assert!(scope.is_allowed("C:\\Windows\\system.ini"));
593 assert!(scope.is_allowed("C:\\NonExistentFile"));
594 assert!(!scope.is_allowed("D:\\home"));
595 }
596
597 let scope = new_scope();
598 {
599 scope.allow_directory("\\\\?\\C:\\", true).unwrap();
601 assert!(scope.is_allowed("C:\\Windows"));
602 assert!(scope.is_allowed("C:\\Windows\\system.ini"));
603 assert!(scope.is_allowed("C:\\NonExistentFile"));
604 assert!(!scope.is_allowed("D:\\home"));
605 }
606
607 let scope = new_scope();
608 {
609 scope.allow_file("\\\\?\\anyfile").unwrap();
611 assert!(scope.is_allowed("\\\\?\\anyfile"));
612 assert!(!scope.is_allowed("\\\\?\\otherfile"));
613 }
614 }
615
616 #[test]
617 fn push_pattern_generated_paths() {
618 macro_rules! assert_pattern {
619 ($patterns:ident, $pattern:literal) => {
620 assert!($patterns.contains(&Pattern::new($pattern).unwrap()))
621 };
622 }
623
624 let mut patterns = HashSet::new();
625
626 #[cfg(not(windows))]
627 {
628 push_pattern(&mut patterns, "/path/to/dir/", Pattern::new).expect("failed to push pattern");
629 push_pattern(&mut patterns, "/path/to/dir/**", Pattern::new).expect("failed to push pattern");
630
631 assert_pattern!(patterns, "/path/to/dir");
632 assert_pattern!(patterns, "/path/to/dir/**");
633 }
634
635 #[cfg(windows)]
636 {
637 push_pattern(&mut patterns, "C:\\path\\to\\dir", Pattern::new)
638 .expect("failed to push pattern");
639 push_pattern(&mut patterns, "C:\\path\\to\\dir\\**", Pattern::new)
640 .expect("failed to push pattern");
641
642 assert_pattern!(patterns, "C:\\path\\to\\dir");
643 assert_pattern!(patterns, "C:\\path\\to\\dir\\**");
644 assert_pattern!(patterns, "\\\\?\\C:\\path\\to\\dir");
645 assert_pattern!(patterns, "\\\\?\\C:\\path\\to\\dir\\**");
646 }
647 }
648}