tauri_plugin_persisted_scope/
lib.rs1#![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
31const SCOPE_STATE_FILENAME: &str = ".persisted-scope";
33#[cfg(feature = "protocol-asset")]
34const ASSET_SCOPE_STATE_FILENAME: &str = ".persisted-scope-asset";
35
36const 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 let _ = scope.allow_directory(fix_directory(path), false);
119 }
120 TargetType::RecursiveDirectory => {
121 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> {
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 let ac = AhoCorasick::new(PATTERNS).unwrap();
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 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 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}