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 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
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> {
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 let ac = AhoCorasick::new(PATTERNS).unwrap();
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 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 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}