use std::collections::HashMap;
use std::sync::Arc;
use parking_lot::RwLock;
use wasmtime::{Caller, Linker, Memory};
use super::runtime::PluginError;
type KvStore = HashMap<String, HashMap<Vec<u8>, Vec<u8>>>;
#[derive(Default)]
struct KvState {
store: KvStore,
total_bytes: usize,
}
#[derive(Clone, Default)]
pub struct KvBackend {
inner: Arc<RwLock<KvState>>,
max_value_bytes: usize,
max_keys_per_plugin: usize,
max_plugins: usize,
max_total_bytes: usize,
}
impl KvBackend {
pub fn new() -> Self {
Self::default()
}
pub fn with_limits(
max_value_bytes: usize,
max_keys_per_plugin: usize,
max_plugins: usize,
max_total_bytes: usize,
) -> Self {
Self {
max_value_bytes,
max_keys_per_plugin,
max_plugins,
max_total_bytes,
..Self::default()
}
}
pub fn max_value_bytes(&self) -> usize {
self.max_value_bytes
}
pub fn get(&self, plugin: &str, key: &[u8]) -> Option<Vec<u8>> {
let g = self.inner.read();
g.store.get(plugin).and_then(|m| m.get(key).cloned())
}
pub fn set(&self, plugin: &str, key: Vec<u8>, value: Vec<u8>) -> bool {
if self.max_value_bytes != 0
&& (key.len() > self.max_value_bytes || value.len() > self.max_value_bytes)
{
return false;
}
let mut g = self.inner.write();
let ns_exists = g.store.contains_key(plugin);
if self.max_plugins != 0 && !ns_exists && g.store.len() >= self.max_plugins {
return false;
}
let old_val_len = g
.store
.get(plugin)
.and_then(|m| m.get(&key))
.map(|v| v.len());
let key_exists = old_val_len.is_some();
if self.max_keys_per_plugin != 0 && !key_exists {
let cur = g.store.get(plugin).map(|m| m.len()).unwrap_or(0);
if cur >= self.max_keys_per_plugin {
return false;
}
}
let mut delta = value.len() as isize - old_val_len.unwrap_or(0) as isize;
if !key_exists {
delta += key.len() as isize;
}
if !ns_exists {
delta += plugin.len() as isize;
}
if self.max_total_bytes != 0
&& g.total_bytes as isize + delta > self.max_total_bytes as isize
{
return false;
}
g.store
.entry(plugin.to_string())
.or_default()
.insert(key, value);
g.total_bytes = (g.total_bytes as isize + delta) as usize;
true
}
pub fn delete(&self, plugin: &str, key: &[u8]) {
let mut g = self.inner.write();
let removed = match g.store.get_mut(plugin) {
Some(m) => {
let val_len = m.remove(key).map(|v| v.len());
val_len.map(|len| (len, m.is_empty()))
}
None => None,
};
if let Some((val_len, now_empty)) = removed {
g.total_bytes = g.total_bytes.saturating_sub(key.len() + val_len);
if now_empty {
g.store.remove(plugin);
g.total_bytes = g.total_bytes.saturating_sub(plugin.len());
}
}
}
pub fn len(&self, plugin: &str) -> usize {
self.inner
.read()
.store
.get(plugin)
.map(|m| m.len())
.unwrap_or(0)
}
pub fn list_keys(&self, plugin: &str, prefix: &[u8]) -> Vec<String> {
let g = self.inner.read();
g.store
.get(plugin)
.map(|m| {
m.keys()
.filter(|k| k.starts_with(prefix))
.map(|k| String::from_utf8_lossy(k).into_owned())
.collect()
})
.unwrap_or_default()
}
}
pub struct StoreCtx {
pub plugin_name: String,
pub kv: KvBackend,
}
pub fn register_kv_imports(linker: &mut Linker<StoreCtx>) -> Result<(), PluginError> {
linker
.func_wrap(
"env",
"kv_get",
|mut caller: Caller<'_, StoreCtx>,
key_ptr: i32,
key_len: i32,
val_out_ptr: i32,
val_max_len: i32|
-> i32 {
let memory = match get_memory(&mut caller) {
Some(m) => m,
None => return -1,
};
let key = match read_bytes(&memory, &caller, key_ptr, key_len) {
Some(b) => b,
None => return -1,
};
let plugin_name = caller.data().plugin_name.clone();
let kv = caller.data().kv.clone();
let value = match kv.get(&plugin_name, &key) {
Some(v) => v,
None => return -1,
};
if (value.len() as i32) > val_max_len {
return -2;
}
if write_bytes(&memory, &mut caller, val_out_ptr, &value).is_err() {
return -1;
}
value.len() as i32
},
)
.map_err(|e| PluginError::RuntimeError(format!("link kv_get: {}", e)))?;
linker
.func_wrap(
"env",
"kv_set",
|mut caller: Caller<'_, StoreCtx>,
key_ptr: i32,
key_len: i32,
val_ptr: i32,
val_len: i32|
-> i32 {
let memory = match get_memory(&mut caller) {
Some(m) => m,
None => return -1,
};
let key = match read_bytes(&memory, &caller, key_ptr, key_len) {
Some(b) => b,
None => return -1,
};
let val = match read_bytes(&memory, &caller, val_ptr, val_len) {
Some(b) => b,
None => return -1,
};
let plugin_name = caller.data().plugin_name.clone();
let kv = caller.data().kv.clone();
if kv.set(&plugin_name, key, val) {
0
} else {
-1
}
},
)
.map_err(|e| PluginError::RuntimeError(format!("link kv_set: {}", e)))?;
linker
.func_wrap(
"env",
"kv_delete",
|mut caller: Caller<'_, StoreCtx>, key_ptr: i32, key_len: i32| -> i32 {
let memory = match get_memory(&mut caller) {
Some(m) => m,
None => return -1,
};
let key = match read_bytes(&memory, &caller, key_ptr, key_len) {
Some(b) => b,
None => return -1,
};
let plugin_name = caller.data().plugin_name.clone();
let kv = caller.data().kv.clone();
kv.delete(&plugin_name, &key);
0
},
)
.map_err(|e| PluginError::RuntimeError(format!("link kv_delete: {}", e)))?;
Ok(())
}
pub fn register_crypto_imports(linker: &mut Linker<StoreCtx>) -> Result<(), PluginError> {
use sha2::{Digest, Sha256};
linker
.func_wrap(
"env",
"sha256_hex",
|mut caller: Caller<'_, StoreCtx>, in_ptr: i32, in_len: i32, out_ptr: i32| -> i32 {
let memory = match get_memory(&mut caller) {
Some(m) => m,
None => return -1,
};
let input = match read_bytes(&memory, &caller, in_ptr, in_len) {
Some(b) => b,
None => return -1,
};
let digest = Sha256::digest(&input);
let mut hex = [0u8; 64];
const HEX: &[u8; 16] = b"0123456789abcdef";
for (i, b) in digest.iter().enumerate() {
hex[i * 2] = HEX[(b >> 4) as usize];
hex[i * 2 + 1] = HEX[(b & 0x0f) as usize];
}
if write_bytes(&memory, &mut caller, out_ptr, &hex).is_err() {
return -1;
}
64
},
)
.map_err(|e| PluginError::RuntimeError(format!("link sha256_hex: {}", e)))?;
Ok(())
}
fn get_memory(caller: &mut Caller<'_, StoreCtx>) -> Option<Memory> {
caller.get_export("memory").and_then(|e| e.into_memory())
}
fn read_bytes(
memory: &Memory,
caller: &Caller<'_, StoreCtx>,
ptr: i32,
len: i32,
) -> Option<Vec<u8>> {
if len < 0 {
return None;
}
let start = ptr as usize;
let end = start.checked_add(len as usize)?;
let data = memory.data(caller);
data.get(start..end).map(|s| s.to_vec())
}
fn write_bytes(
memory: &Memory,
caller: &mut Caller<'_, StoreCtx>,
ptr: i32,
bytes: &[u8],
) -> Result<(), ()> {
let start = ptr as usize;
let end = start.checked_add(bytes.len()).ok_or(())?;
let data = memory.data_mut(caller);
let slot = data.get_mut(start..end).ok_or(())?;
slot.copy_from_slice(bytes);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kv_namespaced_per_plugin() {
let kv = KvBackend::new();
kv.set("plugin-a", b"k".to_vec(), b"v1".to_vec());
kv.set("plugin-b", b"k".to_vec(), b"v2".to_vec());
assert_eq!(kv.get("plugin-a", b"k"), Some(b"v1".to_vec()));
assert_eq!(kv.get("plugin-b", b"k"), Some(b"v2".to_vec()));
assert_eq!(kv.get("plugin-c", b"k"), None);
}
#[test]
fn kv_overwrite_is_idempotent() {
let kv = KvBackend::new();
kv.set("p", b"k".to_vec(), b"v1".to_vec());
kv.set("p", b"k".to_vec(), b"v2".to_vec());
assert_eq!(kv.get("p", b"k"), Some(b"v2".to_vec()));
assert_eq!(kv.len("p"), 1);
}
#[test]
fn kv_delete_idempotent_on_missing() {
let kv = KvBackend::new();
kv.delete("p", b"never-set");
kv.set("p", b"k".to_vec(), b"v".to_vec());
kv.delete("p", b"k");
assert_eq!(kv.get("p", b"k"), None);
}
#[test]
fn kv_list_keys_empty_namespace_is_empty() {
let kv = KvBackend::new();
assert!(kv.list_keys("nobody", b"").is_empty());
}
#[test]
fn kv_list_keys_filters_by_prefix() {
let kv = KvBackend::new();
kv.set("p", b"budget/a".to_vec(), b"1".to_vec());
kv.set("p", b"budget/b".to_vec(), b"2".to_vec());
kv.set("p", b"region_map".to_vec(), b"3".to_vec());
let mut all = kv.list_keys("p", b"");
all.sort();
assert_eq!(all, vec!["budget/a", "budget/b", "region_map"]);
let mut budget = kv.list_keys("p", b"budget/");
budget.sort();
assert_eq!(budget, vec!["budget/a", "budget/b"]);
}
#[test]
fn kv_value_cap_rejects_oversized_value() {
let kv = KvBackend::with_limits(4, 0, 0, 0);
assert!(kv.set("p", b"k".to_vec(), b"1234".to_vec()));
assert!(!kv.set("p", b"k".to_vec(), b"12345".to_vec()));
assert_eq!(kv.get("p", b"k"), Some(b"1234".to_vec()));
}
#[test]
fn kv_value_cap_also_bounds_key_length() {
let kv = KvBackend::with_limits(4, 0, 0, 0);
assert!(kv.set("p", b"kkkk".to_vec(), b"v".to_vec()));
assert!(!kv.set("p", b"kkkkk".to_vec(), b"v".to_vec()));
assert_eq!(kv.get("p", b"kkkkk"), None);
assert_eq!(kv.len("p"), 1);
}
#[test]
fn kv_namespace_cap_blocks_new_plugins_but_allows_existing() {
let kv = KvBackend::with_limits(0, 0, 2, 0);
assert!(kv.set("a", b"k".to_vec(), b"1".to_vec()));
assert!(kv.set("b", b"k".to_vec(), b"2".to_vec()));
assert!(!kv.set("c", b"k".to_vec(), b"3".to_vec()));
assert_eq!(kv.get("c", b"k"), None);
assert!(kv.list_keys("c", b"").is_empty());
assert!(kv.set("a", b"k2".to_vec(), b"9".to_vec()));
assert_eq!(kv.len("a"), 2);
}
#[test]
fn kv_delete_reclaims_empty_namespace_slot() {
let kv = KvBackend::with_limits(0, 0, 1, 0);
assert!(kv.set("a", b"k".to_vec(), b"1".to_vec()));
assert!(!kv.set("b", b"k".to_vec(), b"2".to_vec()));
kv.delete("a", b"k");
assert_eq!(kv.len("a"), 0);
assert!(kv.set("b", b"k".to_vec(), b"2".to_vec()));
assert_eq!(kv.get("b", b"k"), Some(b"2".to_vec()));
}
#[test]
fn kv_key_count_cap_blocks_new_keys_but_allows_overwrite() {
let kv = KvBackend::with_limits(0, 2, 0, 0);
assert!(kv.set("p", b"a".to_vec(), b"1".to_vec()));
assert!(kv.set("p", b"b".to_vec(), b"2".to_vec()));
assert!(!kv.set("p", b"c".to_vec(), b"3".to_vec()));
assert_eq!(kv.len("p"), 2);
assert!(kv.set("p", b"a".to_vec(), b"updated".to_vec()));
assert_eq!(kv.get("p", b"a"), Some(b"updated".to_vec()));
assert_eq!(kv.len("p"), 2);
}
#[test]
fn kv_zero_caps_mean_unlimited() {
let kv = KvBackend::with_limits(0, 0, 0, 0);
assert!(kv.set("p", b"big".to_vec(), vec![0u8; 1_000_000]));
for i in 0..1000u32 {
assert!(kv.set("p", i.to_le_bytes().to_vec(), b"v".to_vec()));
}
assert_eq!(kv.len("p"), 1001);
}
#[test]
fn kv_total_bytes_cap_counts_key_value_and_namespace() {
let kv = KvBackend::with_limits(0, 0, 0, 10);
assert!(kv.set("p", b"k".to_vec(), b"abc".to_vec()));
assert!(kv.set("p", b"k2".to_vec(), b"xy".to_vec()));
assert!(!kv.set("p", b"z".to_vec(), b"q".to_vec()));
assert_eq!(kv.get("p", b"z"), None);
assert_eq!(kv.len("p"), 2);
}
#[test]
fn kv_total_bytes_cap_charges_overwrite_value_delta_only() {
let kv = KvBackend::with_limits(0, 0, 0, 8);
assert!(kv.set("p", b"k".to_vec(), b"aa".to_vec()));
assert!(kv.set("p", b"k".to_vec(), b"aaaa".to_vec()));
assert_eq!(kv.get("p", b"k"), Some(b"aaaa".to_vec()));
assert!(!kv.set("p", b"k".to_vec(), b"aaaaaaa".to_vec()));
assert_eq!(kv.get("p", b"k"), Some(b"aaaa".to_vec()));
}
#[test]
fn kv_total_bytes_cap_reclaimed_on_delete() {
let kv = KvBackend::with_limits(0, 0, 0, 6);
assert!(kv.set("p", b"k".to_vec(), b"aaaa".to_vec()));
assert!(!kv.set("p", b"k2".to_vec(), b"z".to_vec()));
kv.delete("p", b"k");
assert!(kv.set("q", b"k".to_vec(), b"aaaa".to_vec()));
assert_eq!(kv.get("q", b"k"), Some(b"aaaa".to_vec()));
}
}