tauri_plugin_persisted_scope/
lib.rs1#![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
27const SCOPE_STATE_FILENAME: &str = ".persisted-scope";
29#[cfg(feature = "protocol-asset")]
30const ASSET_SCOPE_STATE_FILENAME: &str = ".persisted-scope-asset";
31
32const 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 let _ = scope.allow_directory(fix_directory(path), false);
115 }
116 TargetType::RecursiveDirectory => {
117 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 let ac = AhoCorasick::new(PATTERNS).unwrap();
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 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 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}