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    plugin::{Builder, TauriPlugin},
21    Manager, Runtime,
22};
23use tauri_plugin_fs::FsExt;
24
25use std::{
26    fs::{create_dir_all, File},
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
166pub fn init<R: Runtime>() -> TauriPlugin<R> {
167    Builder::new("persisted-scope")
168        .setup(|app, _api| {
169            let fs_scope = app.try_fs_scope();
170            #[cfg(feature = "protocol-asset")]
171            let asset_protocol_scope = app.asset_protocol_scope();
172            let app = app.clone();
173            let app_dir = app.path().app_data_dir();
174
175            if let Ok(app_dir) = app_dir {
176                let fs_scope_state_path = app_dir.join(SCOPE_STATE_FILENAME);
177                #[cfg(feature = "protocol-asset")]
178                let asset_scope_state_path = app_dir.join(ASSET_SCOPE_STATE_FILENAME);
179
180                if let Some(fs_scope) = &fs_scope {
181                     let _ = fs_scope.forbid_file(&fs_scope_state_path);
182                } else {
183                    #[cfg(debug_assertions)]
184                    eprintln!("Please make sure to register the `fs` plugin before the `persisted-scope` plugin!");
185                }
186                #[cfg(feature = "protocol-asset")]
187                let _ = asset_protocol_scope.forbid_file(&asset_scope_state_path);
188
189                // We're trying to fix broken .persisted-scope files seamlessly, so we'll be running this on the values read on the saved file.
190                // 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.
191                let ac = AhoCorasick::new(PATTERNS).unwrap(/* This should be impossible to fail since we're using a small static input */);
192
193                if let Some(fs_scope) = &fs_scope {
194                    if fs_scope_state_path.exists() {
195                        let scope: Scope = std::fs::read(&fs_scope_state_path)
196                            .map_err(Error::from)
197                            .and_then(|scope| bincode::deserialize(&scope).map_err(Into::into))
198                            .unwrap_or_default();
199
200                        for allowed in &scope.allowed_paths {
201                            let allowed = fix_pattern(&ac, allowed);
202                            allow_path(fs_scope, &allowed);
203                        }
204                        for forbidden in &scope.forbidden_patterns {
205                            let forbidden = fix_pattern(&ac, forbidden);
206                            forbid_path(fs_scope, &forbidden);
207                        }
208
209                        // Manually save the fixed scopes to disk once.
210                        // This is needed to fix broken .peristed-scope files in case the app doesn't update the scope itself.
211                        save_scopes(fs_scope, &app_dir, &fs_scope_state_path);
212                    }
213                }
214
215                #[cfg(feature = "protocol-asset")]
216                if asset_scope_state_path.exists() {
217                    let scope: Scope = std::fs::read(&asset_scope_state_path)
218                        .map_err(Error::from)
219                        .and_then(|scope| bincode::deserialize(&scope).map_err(Into::into))
220                        .unwrap_or_default();
221
222                    for allowed in &scope.allowed_paths {
223                        let allowed = fix_pattern(&ac, allowed);
224                        allow_path(&asset_protocol_scope, &allowed);
225                    }
226                    for forbidden in &scope.forbidden_patterns {
227                        let forbidden = fix_pattern(&ac, forbidden);
228                        forbid_path(&asset_protocol_scope, &forbidden);
229                    }
230
231                    // Manually save the fixed scopes to disk once.
232                    save_scopes(&asset_protocol_scope, &app_dir, &asset_scope_state_path);
233                }
234
235                #[cfg(feature = "protocol-asset")]
236                let app_dir_ = app_dir.clone();
237
238                if let Some(fs_scope) = &fs_scope {
239                    fs_scope.listen(move |event| {
240                        if let tauri::fs::Event::PathAllowed(_) = event {
241                            save_scopes(&app.fs_scope(), &app_dir, &fs_scope_state_path);
242                        }
243                    });
244                }
245
246                #[cfg(feature = "protocol-asset")]
247                {
248                    let asset_protocol_scope_ = asset_protocol_scope.clone();
249                    asset_protocol_scope.listen(move |event| {
250                        if let tauri::scope::fs::Event::PathAllowed(_) = event {
251                            save_scopes(&asset_protocol_scope_, &app_dir_, &asset_scope_state_path);
252                        }
253                    });
254                }
255            }
256            Ok(())
257        })
258        .build()
259}