fix(plugins): allow 64 MiB of persistent storage per plugin (#1967)

This commit is contained in:
Arnaud Ritti authored and GitHub committed 2026-11-07 18:51:23 +08:00
1 parent 303d5950bf
commit 17a121e7f5
48 files changed
+480 -24

No files matched your search

+39 -2
View File
@@ -18,7 +18,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
/// Largest storage of one plugin (keys + values).
const MAX_STORAGE: usize = 1 << 20;
const MAX_STORAGE: usize = 64 << 20;
const MAX_KEY: usize = 256;
/// Most status bar items per plugin.
const MAX_STATUS_ITEMS: usize = 8;
@@ -856,7 +856,10 @@ impl HostApi for PluginApi {
}
let size: usize = map.iter().map(|(k, v)| k.len() + v.len()).sum();
if size > MAX_STORAGE {
return Err(HostError::Invalid(format!("storage is limited to {} KiB", MAX_STORAGE >> 10)));
return Err(HostError::Invalid(format!(
"storage is limited to {} MiB (UTF-8 keys and values)",
MAX_STORAGE >> 20
)));
}
self.save_storage(&map)
}
@@ -1292,6 +1295,40 @@ mod tests {
PluginApi { pkg: Package::new(manifest, dir.to_path_buf()), shared }
}
#[test]
fn storage_keeps_multimegabyte_indexes_after_host_recreation() {
let tmp = tempfile::tempdir().unwrap();
let value = "é".repeat(2 << 20);
{
let api = api(tmp.path(), Arc::default());
api.storage_set("index", Some(value.clone())).unwrap();
}
let api = api(tmp.path(), Arc::default());
assert_eq!(api.storage_get("index").unwrap().as_deref(), Some(value.as_str()));
api.storage_set("index", None).unwrap();
assert_eq!(api.storage_get("index").unwrap(), None);
}
#[test]
fn storage_quota_counts_utf8_keys_and_values_and_preserves_data_on_failure() {
let tmp = tempfile::tempdir().unwrap();
let api = api(tmp.path(), Arc::default());
let key = "index";
let mut value = "é".repeat(((64 << 20) - key.len()) / 2);
value.push('x');
api.storage_set(key, Some(value.clone())).unwrap();
for (key, value) in [("x", String::new()), (key, format!("{value}x"))] {
let err = api.storage_set(key, Some(value)).unwrap_err();
assert!(matches!(err, HostError::Invalid(ref message) if message.contains("64 MiB")), "{err}");
}
assert_eq!(api.storage_keys("").unwrap(), [key]);
assert_eq!(api.storage_get(key).unwrap().as_deref(), Some(value.as_str()));
api.storage_set(key, Some("small".into())).unwrap();
api.storage_set("other", Some("value".into())).unwrap();
assert_eq!(api.storage_get("other").unwrap().as_deref(), Some("value"));
}
#[test]
fn repository_and_search_paths_match_ipc_events() {
let tmp = tempfile::tempdir().unwrap();
+21
View File
@@ -1540,6 +1540,27 @@ fn official_key_promoter() {
assert!(f.host.event_kinds().is_empty());
}
#[test]
fn storage_keeps_multimegabyte_indexes_after_idle_reclaim_and_removes_them_on_uninstall() {
let f = fixture(Limits { idle_timeout: Some(Duration::from_millis(100)), ..Limits::default() });
let pkg = Path::new(env!("CARGO_MANIFEST_DIR")).join("../ide-plugin/tests/fixtures/storage");
f.install(&pkg);
f.run("test.storage", "save", None).unwrap();
assert_eq!(f.host.instances(), 1);
std::thread::sleep(Duration::from_millis(150));
f.host.tick();
assert_eq!(f.host.instances(), 0);
assert_eq!(f.host.info_of("test.storage").unwrap().stats.reclaims, 1);
assert_eq!(f.run("test.storage", "load", None).unwrap().as_deref(), Some("4194304"));
assert_eq!(f.host.info_of("test.storage").unwrap().stats.activations, 2);
let storage_dir = f.plugins_dir.join(".data/test.storage");
assert!(storage_dir.join("storage.json").is_file());
f.host.uninstall("test.storage").unwrap();
assert!(!storage_dir.exists());
f.install(&pkg);
assert!(f.run("test.storage", "load", None).unwrap_err().contains("missing index"));
}
fn git(dir: &Path, args: &[&str]) {
let out = std::process::Command::new("git")
.args(["-c", "user.name=t", "-c", "user.email=t@t", "-c", "init.defaultBranch=main"])
Binary file not shown.
@@ -1 +1 @@
e9f2e3818dab64aa24e3f1852eff301c2ba971e90157067b11b832cdcc361fd6
83a1e3f362740752ed69c53da29ff950cf9c9de18125af6c98995ea490b31690
Binary file not shown.
@@ -1 +1 @@
ad6f103e549a51cc64aa9ce03e0759c7e247b98fa4d7d84f2d5cbbd32a56e47a
a9f1e2ec87de32ef241fcb559d157cd05800dcf106ab27e906ba36468c0b39e5
Binary file not shown.
@@ -1 +1 @@
bb5747f6e319f1852dbfd92c6a65e6e54a41a6d508fe96dc570a5c0684f7d322
d065af9f54b5bd1ec2fbc31250c87153a55501a4e8bf4002fcc04c67c2d1e03e
Binary file not shown.
@@ -1 +1 @@
f651ab979bc6b52c5bdde0417667393ffd442726b406b9064a431e0fad39b8be
dd57b09293deb0fdc3201978e350c118f428429df4beccc4f23665be0335a795
Binary file not shown.
@@ -1 +1 @@
7fd84d18cbcf8673c988e7d3d9a143883635f967ba2b3f679fb1f3aafaa9169c
436516e5f17a11f5a1225b854ab9fef703172a47d33bf3d840c721ad6d373e62
Binary file not shown.
@@ -1 +1 @@
a2762b47de8abd3d1c4fff0130001c6a9347292f94bfffd3933db1a270c95076
4e8118c9f4974385421d1e2ca0106fd658ad85124e1453c4114f60a164f72f68
Binary file not shown.
@@ -1 +1 @@
a38d39b7d43d4f2fbcb31064a9037f3a6d58fb16b3e7bfeb0f85f5f13459dbff
b77e7e3d163995f0a9a3f3cb49dc6c4ea04b2eaa0a8b74b46c64e83908bae504
+333
View File
@@ -0,0 +1,333 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anyhow"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "bitflags"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "equivalent"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
dependencies = [
"foldhash",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
"serde",
"serde_core",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "log"
version = "0.4.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c"
[[package]]
name = "macro-string"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "openidea-plugin"
version = "0.1.0"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "openidea-test-storage"
version = "0.1.0"
dependencies = [
"openidea-plugin",
]
[[package]]
name = "prettyplease"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "syn"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
[[package]]
name = "wasm-encoder"
version = "0.262.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4495a6f85dd94c411c749303efcbc5defade0b0cbdbc0541495241ab917a9b5a"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.262.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c86ff6668190d9fb70372b306b4f9fe8930f0e2f57ce72e4922bf2f117900949"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.262.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "974d643b125b272607e2fff8012ea14a8328acd7621e4bafa7afb32500052b1b"
dependencies = [
"bitflags",
"hashbrown",
"indexmap",
"semver",
]
[[package]]
name = "wit-bindgen"
version = "0.63.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b85a987ab61d365382d25c7d47e26edd48941b3bf6e3fbf76eff918bc71dab06"
dependencies = [
"bitflags",
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen-core"
version = "0.63.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "775e122e390603abfacd548027016cd0113a6e2bf2f8ce6bdd3658b013456ea8"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.63.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84e26e13b9d972026778d2ffb7985736de116026fdc8d9464f966e12df2eb240"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.63.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16a008a0d6b4840677326beb4edbeb210a2f1b7d0183fb7cc33f466b90fd7fb6"
dependencies = [
"anyhow",
"macro-string",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.262.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "078def49fdb49cab405c3006e05793b3fdaf7069126ba6b9c97d01ec8c98fe4c"
dependencies = [
"anyhow",
"bitflags",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.262.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00f0dc051466ffe46787721fca3d088536f158ae400be044f1b1c386ba9397ad"
dependencies = [
"anyhow",
"hashbrown",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-ident",
"wasmparser",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "openidea-test-storage"
version = "0.1.0"
edition = "2024"
publish = false
[workspace]
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
openidea-plugin = { path = "../../../../../sdk/rust/openidea-plugin" }
[profile.release]
opt-level = "s"
lto = true
strip = true
codegen-units = 1
panic = "abort"
Binary file not shown.
@@ -0,0 +1 @@
9f2195fb83f16221e98313c095d57c293e79041ed8ae439fe2fb4f008a3a0921
+16
View File
@@ -0,0 +1,16 @@
id = "test.storage"
name = "Storage fixture"
version = "0.1.0"
engines.openidea = "^1.3"
wasm = "dist/plugin.wasm"
[capabilities]
required = ["storage"]
[[contributes.commands]]
id = "save"
title = "Save index"
[[contributes.commands]]
id = "load"
title = "Load index"
+25
View File
@@ -0,0 +1,25 @@
use openidea_plugin::{Error, Invocation, Plugin, host, register_plugin};
#[derive(Default)]
struct Storage;
impl Plugin for Storage {
fn run_command(&mut self, id: &str, _: &Invocation, _: Option<&str>) -> Result<Option<String>, Error> {
match id {
"save" => {
host::storage::set("index", &"é".repeat(2 << 20))?;
Ok(None)
}
"load" => {
let value = host::storage::get("index")?.ok_or("missing index")?;
if value != "é".repeat(2 << 20) {
return Err("index changed".into());
}
Ok(Some(value.len().to_string()))
}
_ => Err(Error::NotFound(id.into())),
}
}
}
register_plugin!(Storage);
+1 -1
View File
@@ -239,7 +239,7 @@ machine. Users can revoke optional capabilities in Settings › Plugins.
| `editor.write` | medium | `TextEditor::apply_edits` |
| `vcs.read` | low | `Project::vcs_status` (branch, upstream, ahead/behind, changes), `vcs_remotes`, `host::pending_commit`, before-commit checks |
| `vcs.write` | medium | `Project::vcs_fetch` (trusted projects only), `host::set_commit_message` |
| `storage` | low | `host::storage` (1 MiB per plugin) |
| `storage` | low | `host::storage` (64 MiB per plugin, counting UTF-8 bytes of keys and values) |
| `events` | low | file opened/saved, editor focused, repository changed events |
| `events.input` | medium | `action-performed` events (which actions you run, and how) |
| `process:<program>` | high | `Project::run_process`, language servers, debug adapters, runs |
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
c9cbe5d5a6d2d5bf3bda9308b6e07c374a60b3f95714425e9f0d855bcaa38666
a5cba00850ccde12896ed7a69ae336a1968e024e8f5fefb6a49f2adea10569e4
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
dada583da1a117277325077bf700a2b485433768836335683ff3f4d370e43334
27d61fcc109561aeea56f3a37da0aba7b6c56fdf01ff918f2e62d5732832ba3d
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
720fc0e60bb11cabbf9658bbada16eb889e509ab436d62b17b2a64ef9651a559
c86fae61f88b6903354b1fb706b0718f9036f82085653481d77ddc3f622b3a73
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
4ced6756e3f94c0989a7c00948c3456954eaa0b08cb072a3b1790872eabc6a45
b4d059850083339f826223700eda7df0c8b655a40e4832d3acda36dc0f76db9a
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
ae7c04be3b7811e560c711077391bec14705a1acb769ba8a7e0212dc8e8ec5ff
b2977b3b82ad26126125c614281cbbf888bb8151ece9a43fbaa5459c7a8b97cc
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
df4f523b86e29e7f5f992eeb9b8fd9ab61b58ac8b9e05d20f1db457e9328f187
9f59f59b999475a8c4c626f1cfc3c77ef1b3c0684f0cbf9af148556c8f9d30c0
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
0a4d7548b157513276176e4e02ec5d8a5a71f55f4098afd9963ec0f47312d6de
0f175352e4fcfe3b07359afb7f4a20348a488a21012e89775c628d19277d8c8c
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
451c7bf3dcbdd6e57dc657cb7d1f4ddee8d262a859863ac71287b85eb474793c
d7e9b2801ca70df9a84ec458f0fc5c6b01b7d03c056807c53268e40ecbbb9d1a
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
daf6fb2a5cff9210cc380f19dca42480d380166e028239d985abf37ebddd7a6c
50524b5be85f6bc040521f97adba6030263045dcacfeae8dbcf73ce7234e2616
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
1364aed3a99e0769d53b444c51b873083aeb7088afa7dc6c8821cd312d0319ad
0c44311a5f9ddb1893be7c82d818297a05b15576d996a33ddc2389f9432c416c
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
d6d1a81f02c7346b365689bc3893745aa9d867e56c187cdd28efde758bfb0fe8
3e4e23976ce0ddb1e83813269eefc24008521cdf22757e6b5249f6f219f344d2
+2 -1
View File
@@ -749,7 +749,8 @@ pub fn unsubscribe(kind: EventKind) {
imp::unsubscribe(kind)
}
/// The plugin's persistent key-value store (≤ 1 MiB). Needs `storage`.
/// The plugin's persistent key-value store (≤ 64 MiB of UTF-8 keys and values).
/// Needs `storage`; rejected writes preserve the stored data.
pub mod storage {
use super::imp;
use crate::types::Error;
+2 -1
View File
@@ -68,7 +68,8 @@ interface ui {
}
/// Persistent key-value storage of the plugin (survives restarts and idle
/// reclaim; instance memory does not). Requires `storage`. At most 1 MiB.
/// reclaim; instance memory does not). Requires `storage`. At most 64 MiB,
/// counting UTF-8 bytes of keys and values; rejected writes preserve the data.
interface storage {
use types.{error};
+2 -1
View File
@@ -68,7 +68,8 @@ interface ui {
}
/// Persistent key-value storage of the plugin (survives restarts and idle
/// reclaim; instance memory does not). Requires `storage`. At most 1 MiB.
/// reclaim; instance memory does not). Requires `storage`. At most 64 MiB,
/// counting UTF-8 bytes of keys and values; rejected writes preserve the data.
interface storage {
use types.{error};