Skip to content

Commit

Permalink
fix(persisted-scope): Prevent out-of-memory issues, fixes #274 (#328)
Browse files Browse the repository at this point in the history
* fix(persisted-scope): Unescape paths before saving to disk.

* fix cfg flags

* dedupe code

* unescape when reading to try to fix existing files

* add more patterns. only fix pattern on app start. don't check if pattern is allowed already.

* remove dbg log

* typo

* remove unused imports

* clippy

* fix compilation with asset-protocol feature flag enabled

* update patterns

* manually re-save state once

* add changefile

* remove dbg print
  • Loading branch information
FabianLars authored Apr 26, 2023
1 parent 8e5dead commit ebb2eb2
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 29 deletions.
5 changes: 5 additions & 0 deletions .changes/persisted-scope-fix-oom.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
persisted-scope: patch
---

Recursively unescape saved patterns before allowing/forbidding them. This effectively prevents `.persisted-scope` files from blowing up, which caused Out-Of-Memory issues, while automatically fixing existing broken files seamlessly.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions plugins/persisted-scope/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ serde_json.workspace = true
tauri.workspace = true
log.workspace = true
thiserror.workspace = true
aho-corasick = "0.7"
bincode = "1"

[features]
Expand Down
97 changes: 68 additions & 29 deletions plugins/persisted-scope/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,34 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use aho_corasick::AhoCorasick;
use serde::{Deserialize, Serialize};
use tauri::{
plugin::{Builder, TauriPlugin},
FsScopeEvent, Manager, Runtime,
AppHandle, FsScopeEvent, Manager, Runtime,
};

use std::{
fs::{create_dir_all, File},
io::Write,
path::Path,
};

const SCOPE_STATE_FILENAME: &str = ".persisted-scope";

// Most of these patterns are just added to try to fix broken files in the wild.
// After a while we can hopefully reduce it to something like [r"[?]", r"[*]", r"\\?\\\?\"]
const PATTERNS: &[&str] = &[
r"[[]",
r"[]]",
r"[?]",
r"[*]",
r"\?\?",
r"\\?\\?\",
r"\\?\\\?\",
];
const REPLACE_WITH: &[&str] = &[r"[", r"]", r"?", r"*", r"\?", r"\\?\", r"\\?\"];

#[derive(Debug, thiserror::Error)]
enum Error {
#[error(transparent)]
Expand All @@ -33,6 +48,41 @@ struct Scope {
forbidden_patterns: Vec<String>,
}

fn fix_pattern(ac: &AhoCorasick, s: &str) -> String {
let s = ac.replace_all(s, REPLACE_WITH);

if ac.find(&s).is_some() {
return fix_pattern(ac, &s);
}

s
}

fn save_scopes<R: Runtime>(app: &AppHandle<R>, app_dir: &Path, scope_state_path: &Path) {
let fs_scope = app.fs_scope();

let scope = Scope {
allowed_paths: fs_scope
.allowed_patterns()
.into_iter()
.map(|p| p.to_string())
.collect(),
forbidden_patterns: fs_scope
.forbidden_patterns()
.into_iter()
.map(|p| p.to_string())
.collect(),
};

let _ = create_dir_all(app_dir)
.and_then(|_| File::create(scope_state_path))
.map_err(Error::Io)
.and_then(|mut f| {
f.write_all(&bincode::serialize(&scope).map_err(Error::from)?)
.map_err(Into::into)
});
}

pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("persisted-scope")
.setup(|app| {
Expand All @@ -49,49 +99,38 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
#[cfg(feature = "protocol-asset")]
let _ = asset_protocol_scope.forbid_file(&scope_state_path);

// We're trying to fix broken .persisted-scope files seamlessly, so we'll be running this on the values read on the saved file.
// 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.
let ac = AhoCorasick::new_auto_configured(PATTERNS);

if scope_state_path.exists() {
let scope: Scope = tauri::api::file::read_binary(&scope_state_path)
.map_err(Error::from)
.and_then(|scope| bincode::deserialize(&scope).map_err(Into::into))
.unwrap_or_default();
for allowed in &scope.allowed_paths {
// allows the path as is
let _ = fs_scope.allow_file(allowed);
let allowed = fix_pattern(&ac, allowed);

let _ = fs_scope.allow_file(&allowed);
#[cfg(feature = "protocol-asset")]
let _ = asset_protocol_scope.allow_file(allowed);
let _ = asset_protocol_scope.allow_file(&allowed);
}
for forbidden in &scope.forbidden_patterns {
// forbid the path as is
let _ = fs_scope.forbid_file(forbidden);
let forbidden = fix_pattern(&ac, forbidden);

let _ = fs_scope.forbid_file(&forbidden);
#[cfg(feature = "protocol-asset")]
let _ = asset_protocol_scope.forbid_file(forbidden);
let _ = asset_protocol_scope.forbid_file(&forbidden);
}

// Manually save the fixed scopes to disk once.
// This is needed to fix broken .peristed-scope files in case the app doesn't update the scope itself.
save_scopes(&app, &app_dir, &scope_state_path);
}

fs_scope.listen(move |event| {
let fs_scope = app.fs_scope();
if let FsScopeEvent::PathAllowed(_) = event {
let scope = Scope {
allowed_paths: fs_scope
.allowed_patterns()
.into_iter()
.map(|p| p.to_string())
.collect(),
forbidden_patterns: fs_scope
.forbidden_patterns()
.into_iter()
.map(|p| p.to_string())
.collect(),
};
let scope_state_path = scope_state_path.clone();

let _ = create_dir_all(&app_dir)
.and_then(|_| File::create(scope_state_path))
.map_err(Error::Io)
.and_then(|mut f| {
f.write_all(&bincode::serialize(&scope).map_err(Error::from)?)
.map_err(Into::into)
});
save_scopes(&app, &app_dir, &scope_state_path);
}
});
}
Expand Down

0 comments on commit ebb2eb2

Please sign in to comment.