tauri_plugin_persisted_scope/
lib.rs

1// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! Save filesystem and asset scopes and restore them when the app is reopened.
6
7#![doc(
8    html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
9    html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
10)]
11
12use aho_corasick::AhoCorasick;
13use serde::{Deserialize, Serialize};
14
15use tauri::{
16    plugin::{Builder, TauriPlugin},
17    Manager, Runtime,
18};
19use tauri_plugin_fs::FsExt;
20
21use std::{
22    fs::{create_dir_all, File},
23    io::Write,
24    path::Path,
25};
26
27// Using 2 separate files so that we don't have to think about write conflicts and not break backwards compat.
28const SCOPE_STATE_FILENAME: &str = ".persisted-scope";
29#[cfg(feature = "protocol-asset")]
30const ASSET_SCOPE_STATE_FILENAME: &str = ".persisted-scope-asset";
31
32// Most of these patterns are just added to try to fix broken files in the wild.
33// After a while we can hopefully reduce it to something like [r"[?]", r"[*]", r"\\?\\\?\"]
34const PATTERNS: &[&str] = &[
35    r"[[]",
36    r"[]]",
37    r"[?]",
38    r"[*]",
39    r"\?\?",
40    r"\\?\\?\",
41    r"\\?\\\?\",
42];
43const REPLACE_WITH: &[&str] = &[r"[", r"]", r"?", r"*", r"\?", r"\\?\", r"\\?\"];
44
45#[derive(Debug, thiserror::Error)]
46enum Error {
47    #[error(transparent)]
48    Io(#[from] std::io::Error),
49    #[error(transparent)]
50    Tauri(#[from] tauri::Error),
51    #[error(transparent)]
52    Bincode(#[from] Box<bincode::ErrorKind>),
53}
54
55#[derive(Debug, Default, Deserialize, Serialize, Eq, PartialEq, Hash)]
56enum TargetType {
57    #[default]
58    File,
59    Directory,
60    RecursiveDirectory,
61}
62
63#[derive(Debug, Default, Deserialize, Serialize)]
64struct Scope {
65    allowed_paths: Vec<String>,
66    forbidden_patterns: Vec<String>,
67}
68
69fn fix_pattern(ac: &AhoCorasick, s: &str) -> String {
70    let s = ac.replace_all(s, REPLACE_WITH);
71
72    if ac.find(&s).is_some() {
73        return fix_pattern(ac, &s);
74    }
75
76    s
77}
78
79const RESURSIVE_DIRECTORY_SUFFIX: &str = "**";
80const DIRECTORY_SUFFIX: &str = "*";
81
82fn detect_scope_type(scope_state_path: &str) -> TargetType {
83    if scope_state_path.ends_with(RESURSIVE_DIRECTORY_SUFFIX) {
84        TargetType::RecursiveDirectory
85    } else if scope_state_path.ends_with(DIRECTORY_SUFFIX) {
86        TargetType::Directory
87    } else {
88        TargetType::File
89    }
90}
91
92fn fix_directory(path_str: &str) -> &Path {
93    let mut path = Path::new(path_str);
94
95    if path.ends_with(DIRECTORY_SUFFIX) || path.ends_with(RESURSIVE_DIRECTORY_SUFFIX) {
96        path = match path.parent() {
97            Some(value) => value,
98            None => return path,
99        };
100    }
101
102    path
103}
104
105fn allow_path(scope: &tauri::fs::Scope, path: &str) {
106    let target_type = detect_scope_type(path);
107
108    match target_type {
109        TargetType::File => {
110            let _ = scope.allow_file(Path::new(path));
111        }
112        TargetType::Directory => {
113            // We remove the '*' at the end of it, else it will be escaped by the pattern.
114            let _ = scope.allow_directory(fix_directory(path), false);
115        }
116        TargetType::RecursiveDirectory => {
117            // We remove the '**' at the end of it, else it will be escaped by the pattern.
118            let _ = scope.allow_directory(fix_directory(path), true);
119        }
120    }
121}
122
123fn forbid_path(scope: &tauri::fs::Scope, path: &str) {
124    let target_type = detect_scope_type(path);
125
126    match target_type {
127        TargetType::File => {
128            let _ = scope.forbid_file(Path::new(path));
129        }
130        TargetType::Directory => {
131            let _ = scope.forbid_directory(fix_directory(path), false);
132        }
133        TargetType::RecursiveDirectory => {
134            let _ = scope.forbid_directory(fix_directory(path), true);
135        }
136    }
137}
138
139fn save_scopes(scope: &tauri::fs::Scope, app_dir: &Path, scope_state_path: &Path) {
140    let scope = Scope {
141        allowed_paths: scope
142            .allowed_patterns()
143            .into_iter()
144            .map(|p| p.to_string())
145            .collect(),
146        forbidden_patterns: scope
147            .forbidden_patterns()
148            .into_iter()
149            .map(|p| p.to_string())
150            .collect(),
151    };
152
153    let _ = create_dir_all(app_dir)
154        .and_then(|_| File::create(scope_state_path))
155        .map_err(Error::Io)
156        .and_then(|mut f| {
157            f.write_all(&bincode::serialize(&scope).map_err(Error::from)?)
158                .map_err(Into::into)
159        });
160}
161
162pub fn init<R: Runtime>() -> TauriPlugin<R> {
163    Builder::new("persisted-scope")
164        .setup(|app, _api| {
165            let fs_scope = app.try_fs_scope();
166            #[cfg(feature = "protocol-asset")]
167            let asset_protocol_scope = app.asset_protocol_scope();
168            let app = app.clone();
169            let app_dir = app.path().app_data_dir();
170
171            if let Ok(app_dir) = app_dir {
172                let fs_scope_state_path = app_dir.join(SCOPE_STATE_FILENAME);
173                #[cfg(feature = "protocol-asset")]
174                let asset_scope_state_path = app_dir.join(ASSET_SCOPE_STATE_FILENAME);
175
176                if let Some(fs_scope) = &fs_scope {
177                     let _ = fs_scope.forbid_file(&fs_scope_state_path);
178                } else {
179                    #[cfg(debug_assertions)]
180                    eprintln!("Please make sure to register the `fs` plugin before the `persisted-scope` plugin!");
181                }
182                #[cfg(feature = "protocol-asset")]
183                let _ = asset_protocol_scope.forbid_file(&asset_scope_state_path);
184
185                // We're trying to fix broken .persisted-scope files seamlessly, so we'll be running this on the values read on the saved file.
186                // 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.
187                let ac = AhoCorasick::new(PATTERNS).unwrap(/* This should be impossible to fail since we're using a small static input */);
188
189                if let Some(fs_scope) = &fs_scope {
190                    if fs_scope_state_path.exists() {
191                    let scope: Scope = std::fs::read(&fs_scope_state_path)
192                        .map_err(Error::from)
193                        .and_then(|scope| bincode::deserialize(&scope).map_err(Into::into))
194                        .unwrap_or_default();
195
196                    for allowed in &scope.allowed_paths {
197                        let allowed = fix_pattern(&ac, allowed);
198                        allow_path(fs_scope, &allowed);
199                    }
200                    for forbidden in &scope.forbidden_patterns {
201                        let forbidden = fix_pattern(&ac, forbidden);
202                        forbid_path(fs_scope, &forbidden);
203                    }
204
205                    // Manually save the fixed scopes to disk once.
206                    // This is needed to fix broken .peristed-scope files in case the app doesn't update the scope itself.
207                    save_scopes(fs_scope, &app_dir, &fs_scope_state_path);
208                }
209            }
210
211                #[cfg(feature = "protocol-asset")]
212                if asset_scope_state_path.exists() {
213                    let scope: Scope = std::fs::read(&asset_scope_state_path)
214                        .map_err(Error::from)
215                        .and_then(|scope| bincode::deserialize(&scope).map_err(Into::into))
216                        .unwrap_or_default();
217
218                    for allowed in &scope.allowed_paths {
219                        let allowed = fix_pattern(&ac, allowed);
220                        allow_path(&asset_protocol_scope, &allowed);
221                    }
222                    for forbidden in &scope.forbidden_patterns {
223                        let forbidden = fix_pattern(&ac, forbidden);
224                        forbid_path(&asset_protocol_scope, &forbidden);
225                    }
226
227                    // Manually save the fixed scopes to disk once.
228                    save_scopes(&asset_protocol_scope, &app_dir, &asset_scope_state_path);
229                }
230
231                #[cfg(feature = "protocol-asset")]
232                let app_dir_ = app_dir.clone();
233
234                if let Some(fs_scope) = &fs_scope {
235                    let app_ = app.clone();
236                    fs_scope.listen(move |event| {
237                        if let tauri::fs::Event::PathAllowed(_) = event {
238                            save_scopes(&app_.fs_scope(), &app_dir, &fs_scope_state_path);
239                        }
240                    });
241                }
242
243                #[cfg(feature = "protocol-asset")]
244                {
245                    let asset_protocol_scope_ = asset_protocol_scope.clone();
246                    asset_protocol_scope.listen(move |event| {
247                        if let tauri::scope::fs::Event::PathAllowed(_) = event {
248                            save_scopes(&asset_protocol_scope_, &app_dir_, &asset_scope_state_path);
249                        }
250                    });
251                }
252            }
253            Ok(())
254        })
255        .build()
256}