Skip to main content

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