Skip to main content

heliosdb_proxy/plugins/
host_imports.rs

1//! Wasmtime-side host imports exposed to WASM plugins.
2//!
3//! Plugins import these from the `env` module:
4//!
5//! ```wat
6//! (import "env" "kv_get"    (func (param i32 i32 i32 i32) (result i32)))
7//! (import "env" "kv_set"    (func (param i32 i32 i32 i32) (result i32)))
8//! (import "env" "kv_delete" (func (param i32 i32)         (result i32)))
9//! ```
10//!
11//! The KV namespace is per-plugin: each plugin sees only its own
12//! key-value store, keyed off `LoadedPlugin.metadata.name`. State
13//! survives across calls because the `KvBackend` is owned by the
14//! runtime, not the per-call `Store`.
15//!
16//! Return-value conventions (i32):
17//!
18//! - `kv_get`: bytes written, or `-1` for missing key, or `-2` if the
19//!   caller's output buffer is too small (caller can retry with a
20//!   larger buffer; the value is left intact).
21//! - `kv_set`: `0` on success, `-1` on internal error. A configured
22//!   cap breach (`kv_max_value_bytes` / `kv_max_keys_per_plugin` /
23//!   `kv_max_plugins` / `kv_max_total_bytes`) is surfaced through this
24//!   same `-1` — the write is rejected and the store is left unchanged.
25//! - `kv_delete`: `0` (idempotent — no error if the key was absent).
26//!
27//! The implementation is in-process and in-memory. A future slice
28//! can swap the backend for a persistent store (sled, redb, …)
29//! without changing the import surface.
30
31use std::collections::HashMap;
32use std::sync::Arc;
33
34use parking_lot::RwLock;
35use wasmtime::{Caller, Linker, Memory};
36
37use super::runtime::PluginError;
38
39/// KV store type alias: plugin-name -> (key -> value)
40type KvStore = HashMap<String, HashMap<Vec<u8>, Vec<u8>>>;
41
42/// Locked interior of a [`KvBackend`]: the namespaced store plus a
43/// running byte counter kept in lock-step with it. `total_bytes` sums,
44/// across every namespace, each stored `key.len() + value.len()` plus
45/// every live namespace's name `plugin.len()` (counted once per
46/// namespace). Maintaining it incrementally under the same write lock
47/// as the store keeps the `max_total_bytes` check O(1) per `set` /
48/// `delete` instead of walking the whole map on every write.
49#[derive(Default)]
50struct KvState {
51    store: KvStore,
52    total_bytes: usize,
53}
54
55/// In-memory KV backend, namespaced by plugin name. The outer map
56/// is keyed by plugin name; the inner map by user-supplied key.
57///
58/// Four optional caps bound how much a caller (plugin or the
59/// `/admin/kv` endpoint) can store; `0` on any of them means
60/// "unlimited". `new()` / `Default` leave them all at `0` so existing
61/// callers and tests keep the historical unbounded behaviour;
62/// production wires real values via [`KvBackend::with_limits`].
63#[derive(Clone, Default)]
64pub struct KvBackend {
65    inner: Arc<RwLock<KvState>>,
66    /// Max bytes for any single key OR value (`0` = unlimited). BOTH
67    /// the user-supplied key and its value are bounded by this cap so
68    /// neither axis can grow without limit.
69    max_value_bytes: usize,
70    /// Max distinct keys per plugin namespace (`0` = unlimited).
71    /// Overwriting an existing key never trips this cap.
72    max_keys_per_plugin: usize,
73    /// Max distinct plugin namespaces / outer-map entries (`0` =
74    /// unlimited). Bounds how many namespaces a caller can bring into
75    /// existence — notably the `/admin/kv/<plugin>/<key>` endpoint,
76    /// which names an arbitrary `<plugin>` and would otherwise let a
77    /// token-holder grow memory without bound by writing to
78    /// unboundedly-many namespace names. Writing to an already-present
79    /// namespace never trips this cap.
80    max_plugins: usize,
81    /// Max TOTAL retained bytes across ALL namespaces (`0` =
82    /// unlimited), summed as each entry's `key + value` bytes plus each
83    /// live namespace's name bytes. This is the survivable-default
84    /// backstop for the `/admin/kv` surface: even at the maximum
85    /// per-axis product (`max_plugins × max_keys_per_plugin ×
86    /// max_value_bytes`), which can reach tens of GiB, this single cap
87    /// bounds actual retained memory to a tunable ceiling, so a
88    /// token-holding admin caller cannot drive the proxy to an OOM by
89    /// hammering `PUT /admin/kv`. Tracked incrementally in [`KvState`].
90    max_total_bytes: usize,
91}
92
93impl KvBackend {
94    pub fn new() -> Self {
95        Self::default()
96    }
97
98    /// Construct with explicit caps. `0` on any field = unlimited.
99    pub fn with_limits(
100        max_value_bytes: usize,
101        max_keys_per_plugin: usize,
102        max_plugins: usize,
103        max_total_bytes: usize,
104    ) -> Self {
105        Self {
106            max_value_bytes,
107            max_keys_per_plugin,
108            max_plugins,
109            max_total_bytes,
110            ..Self::default()
111        }
112    }
113
114    /// The configured single key/value byte cap (`0` = unlimited).
115    /// Lets a caller (e.g. the `/admin/kv` PUT handler) fast-reject an
116    /// oversized body before allocating an owned copy of it.
117    pub fn max_value_bytes(&self) -> usize {
118        self.max_value_bytes
119    }
120
121    /// Read a value. None if missing.
122    pub fn get(&self, plugin: &str, key: &[u8]) -> Option<Vec<u8>> {
123        let g = self.inner.read();
124        g.store.get(plugin).and_then(|m| m.get(key).cloned())
125    }
126
127    /// Insert / overwrite. Returns `false` (and leaves the store
128    /// untouched) when a configured cap would be exceeded:
129    /// - the key OR value length exceeds `max_value_bytes`, or
130    /// - creating a NEW plugin namespace would push the store past
131    ///   `max_plugins`, or
132    /// - inserting a NEW key would push the namespace past
133    ///   `max_keys_per_plugin`, or
134    /// - the resulting `total_bytes` would exceed `max_total_bytes`.
135    ///
136    /// Overwriting an existing key (or writing another key into an
137    /// already-present namespace) never fails the key-count or
138    /// namespace cap. Every cap is checked BEFORE any mutation, so a
139    /// rejected write leaves the store and the byte counter unchanged.
140    pub fn set(&self, plugin: &str, key: Vec<u8>, value: Vec<u8>) -> bool {
141        // Size cap first — cheap, and lets us bail before locking. Both
142        // the key and the value are bounded so neither can grow without
143        // limit (the admin request line already caps their transport
144        // length, but this makes the retained size tunable).
145        if self.max_value_bytes != 0
146            && (key.len() > self.max_value_bytes || value.len() > self.max_value_bytes)
147        {
148            return false;
149        }
150        let mut g = self.inner.write();
151        // Namespace cap: refuse to bring a NEW plugin namespace into
152        // existence once the outer map is full. Writing to a namespace
153        // that already EXISTS is always allowed (the count stays
154        // constant), so a plugin whose namespace is already present is
155        // never starved. NOTE: a loaded plugin's FIRST write — into a
156        // namespace that does not yet exist — CAN still be refused here
157        // if the cap is already saturated by other namespaces (e.g.
158        // ones an admin caller created via `/admin/kv`); the default
159        // cap (256) is deliberately far above the typical loaded-plugin
160        // count (`max_plugins`, default 20) so this is a corner case,
161        // and `kv_set` surfaces the refusal as `-1`. Checked BEFORE
162        // `entry().or_default()` so a rejected write never leaves an
163        // empty namespace behind.
164        let ns_exists = g.store.contains_key(plugin);
165        if self.max_plugins != 0 && !ns_exists && g.store.len() >= self.max_plugins {
166            return false;
167        }
168        // Inspect the existing entry ONCE: whether the key is present
169        // and, if so, its current value length — so an overwrite is
170        // charged only the value-size delta, never the key/namespace
171        // bytes again.
172        let old_val_len = g
173            .store
174            .get(plugin)
175            .and_then(|m| m.get(&key))
176            .map(|v| v.len());
177        let key_exists = old_val_len.is_some();
178        // Key-count cap applies only to genuinely new keys; an
179        // overwrite keeps the namespace size constant, so allow it.
180        if self.max_keys_per_plugin != 0 && !key_exists {
181            let cur = g.store.get(plugin).map(|m| m.len()).unwrap_or(0);
182            if cur >= self.max_keys_per_plugin {
183                return false;
184            }
185        }
186        // Total-bytes cap: compute the SIGNED byte delta this write
187        // would introduce, then reject if it would push the running
188        // total past the ceiling. A new key adds its key bytes; a new
189        // namespace adds its name bytes; the value contributes
190        // `new_len - old_len` (negative when overwriting with a shorter
191        // value).
192        let mut delta = value.len() as isize - old_val_len.unwrap_or(0) as isize;
193        if !key_exists {
194            delta += key.len() as isize;
195        }
196        if !ns_exists {
197            delta += plugin.len() as isize;
198        }
199        if self.max_total_bytes != 0
200            && g.total_bytes as isize + delta > self.max_total_bytes as isize
201        {
202            return false;
203        }
204        // All caps satisfied — commit the write and advance the counter
205        // in lock-step (fold the possibly-negative delta through isize).
206        g.store
207            .entry(plugin.to_string())
208            .or_default()
209            .insert(key, value);
210        g.total_bytes = (g.total_bytes as isize + delta) as usize;
211        true
212    }
213
214    /// Delete; idempotent. Drops the plugin's inner map and its
215    /// outer-map slot once the namespace becomes empty, so a
216    /// delete-heavy caller actually reclaims memory instead of leaving
217    /// zombie namespaces behind — this also keeps the `max_plugins`
218    /// namespace count honest (a fully-drained namespace frees a slot).
219    /// The reclaimed key/value/namespace bytes are subtracted from the
220    /// `total_bytes` counter so the `max_total_bytes` cap tracks the
221    /// live footprint exactly.
222    pub fn delete(&self, plugin: &str, key: &[u8]) {
223        let mut g = self.inner.write();
224        // Remove the key inside the inner-map borrow, capturing the
225        // reclaimed value length and whether the namespace is now
226        // empty, then drop that borrow before touching the outer map
227        // again so the outer-map removal never overlaps the inner
228        // borrow.
229        let removed = match g.store.get_mut(plugin) {
230            Some(m) => {
231                // Fully finish the mutating `remove` (owned `Option`)
232                // before reborrowing `m` immutably for `is_empty`.
233                let val_len = m.remove(key).map(|v| v.len());
234                val_len.map(|len| (len, m.is_empty()))
235            }
236            None => None,
237        };
238        if let Some((val_len, now_empty)) = removed {
239            // key bytes + value bytes are reclaimed (only if a key was
240            // actually removed).
241            g.total_bytes = g.total_bytes.saturating_sub(key.len() + val_len);
242            if now_empty {
243                // Drop the drained namespace and reclaim its name bytes.
244                g.store.remove(plugin);
245                g.total_bytes = g.total_bytes.saturating_sub(plugin.len());
246            }
247        }
248    }
249
250    /// Returns the number of keys in the plugin's namespace.
251    /// Useful for tests and the admin endpoint.
252    pub fn len(&self, plugin: &str) -> usize {
253        self.inner
254            .read()
255            .store
256            .get(plugin)
257            .map(|m| m.len())
258            .unwrap_or(0)
259    }
260
261    /// List keys (lossy UTF-8) in a plugin's namespace, optionally
262    /// filtered by a byte `prefix` (pass `b""` for all keys). Backs
263    /// the `GET /admin/kv/<plugin>/` list endpoint.
264    pub fn list_keys(&self, plugin: &str, prefix: &[u8]) -> Vec<String> {
265        let g = self.inner.read();
266        g.store
267            .get(plugin)
268            .map(|m| {
269                m.keys()
270                    .filter(|k| k.starts_with(prefix))
271                    .map(|k| String::from_utf8_lossy(k).into_owned())
272                    .collect()
273            })
274            .unwrap_or_default()
275    }
276}
277
278/// Per-call store data: the plugin name (so host imports route to
279/// the right KV namespace) and a clone of the shared KV backend.
280/// Carrying the Arc<KvBackend> by value here is cheap (one atomic
281/// inc) and lets the import functions call `caller.data()` to
282/// retrieve it.
283pub struct StoreCtx {
284    pub plugin_name: String,
285    pub kv: KvBackend,
286}
287
288/// Register all host imports under the `env` module against the
289/// supplied linker. Idempotent — calling twice replaces prior bindings.
290pub fn register_kv_imports(linker: &mut Linker<StoreCtx>) -> Result<(), PluginError> {
291    linker
292        .func_wrap(
293            "env",
294            "kv_get",
295            |mut caller: Caller<'_, StoreCtx>,
296             key_ptr: i32,
297             key_len: i32,
298             val_out_ptr: i32,
299             val_max_len: i32|
300             -> i32 {
301                let memory = match get_memory(&mut caller) {
302                    Some(m) => m,
303                    None => return -1,
304                };
305                let key = match read_bytes(&memory, &caller, key_ptr, key_len) {
306                    Some(b) => b,
307                    None => return -1,
308                };
309                let plugin_name = caller.data().plugin_name.clone();
310                let kv = caller.data().kv.clone();
311                let value = match kv.get(&plugin_name, &key) {
312                    Some(v) => v,
313                    None => return -1,
314                };
315                if (value.len() as i32) > val_max_len {
316                    return -2;
317                }
318                if write_bytes(&memory, &mut caller, val_out_ptr, &value).is_err() {
319                    return -1;
320                }
321                value.len() as i32
322            },
323        )
324        .map_err(|e| PluginError::RuntimeError(format!("link kv_get: {}", e)))?;
325
326    linker
327        .func_wrap(
328            "env",
329            "kv_set",
330            |mut caller: Caller<'_, StoreCtx>,
331             key_ptr: i32,
332             key_len: i32,
333             val_ptr: i32,
334             val_len: i32|
335             -> i32 {
336                let memory = match get_memory(&mut caller) {
337                    Some(m) => m,
338                    None => return -1,
339                };
340                let key = match read_bytes(&memory, &caller, key_ptr, key_len) {
341                    Some(b) => b,
342                    None => return -1,
343                };
344                let val = match read_bytes(&memory, &caller, val_ptr, val_len) {
345                    Some(b) => b,
346                    None => return -1,
347                };
348                let plugin_name = caller.data().plugin_name.clone();
349                let kv = caller.data().kv.clone();
350                // A cap breach (value too big / key-count exceeded)
351                // returns false → surface as -1 ("internal error"),
352                // which is already part of the documented contract.
353                if kv.set(&plugin_name, key, val) {
354                    0
355                } else {
356                    -1
357                }
358            },
359        )
360        .map_err(|e| PluginError::RuntimeError(format!("link kv_set: {}", e)))?;
361
362    linker
363        .func_wrap(
364            "env",
365            "kv_delete",
366            |mut caller: Caller<'_, StoreCtx>, key_ptr: i32, key_len: i32| -> i32 {
367                let memory = match get_memory(&mut caller) {
368                    Some(m) => m,
369                    None => return -1,
370                };
371                let key = match read_bytes(&memory, &caller, key_ptr, key_len) {
372                    Some(b) => b,
373                    None => return -1,
374                };
375                let plugin_name = caller.data().plugin_name.clone();
376                let kv = caller.data().kv.clone();
377                kv.delete(&plugin_name, &key);
378                0
379            },
380        )
381        .map_err(|e| PluginError::RuntimeError(format!("link kv_delete: {}", e)))?;
382
383    Ok(())
384}
385
386/// Register the `env.sha256_hex` host import. Plugins call:
387///
388/// ```text
389/// env.sha256_hex(in_ptr: i32, in_len: i32, out_ptr: i32) -> i32
390/// ```
391///
392/// where `out_ptr` must point to at least 64 bytes inside plugin
393/// memory (the lower-case hex SHA-256 digest is exactly 64 ASCII
394/// chars). Returns 64 on success, -1 on memory error.
395///
396/// The host computes the digest over the plugin-supplied byte range
397/// using the production `sha2` crate; plugins no longer need to
398/// embed their own (placeholder) hash and stay small.
399pub fn register_crypto_imports(linker: &mut Linker<StoreCtx>) -> Result<(), PluginError> {
400    use sha2::{Digest, Sha256};
401
402    linker
403        .func_wrap(
404            "env",
405            "sha256_hex",
406            |mut caller: Caller<'_, StoreCtx>, in_ptr: i32, in_len: i32, out_ptr: i32| -> i32 {
407                let memory = match get_memory(&mut caller) {
408                    Some(m) => m,
409                    None => return -1,
410                };
411                let input = match read_bytes(&memory, &caller, in_ptr, in_len) {
412                    Some(b) => b,
413                    None => return -1,
414                };
415                let digest = Sha256::digest(&input);
416                // Hex-encode into a fixed 64-byte stack buffer so we
417                // don't allocate per call.
418                let mut hex = [0u8; 64];
419                const HEX: &[u8; 16] = b"0123456789abcdef";
420                for (i, b) in digest.iter().enumerate() {
421                    hex[i * 2] = HEX[(b >> 4) as usize];
422                    hex[i * 2 + 1] = HEX[(b & 0x0f) as usize];
423                }
424                if write_bytes(&memory, &mut caller, out_ptr, &hex).is_err() {
425                    return -1;
426                }
427                64
428            },
429        )
430        .map_err(|e| PluginError::RuntimeError(format!("link sha256_hex: {}", e)))?;
431    Ok(())
432}
433
434fn get_memory(caller: &mut Caller<'_, StoreCtx>) -> Option<Memory> {
435    caller.get_export("memory").and_then(|e| e.into_memory())
436}
437
438fn read_bytes(
439    memory: &Memory,
440    caller: &Caller<'_, StoreCtx>,
441    ptr: i32,
442    len: i32,
443) -> Option<Vec<u8>> {
444    if len < 0 {
445        return None;
446    }
447    let start = ptr as usize;
448    let end = start.checked_add(len as usize)?;
449    let data = memory.data(caller);
450    data.get(start..end).map(|s| s.to_vec())
451}
452
453fn write_bytes(
454    memory: &Memory,
455    caller: &mut Caller<'_, StoreCtx>,
456    ptr: i32,
457    bytes: &[u8],
458) -> Result<(), ()> {
459    let start = ptr as usize;
460    let end = start.checked_add(bytes.len()).ok_or(())?;
461    let data = memory.data_mut(caller);
462    let slot = data.get_mut(start..end).ok_or(())?;
463    slot.copy_from_slice(bytes);
464    Ok(())
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    #[test]
472    fn kv_namespaced_per_plugin() {
473        let kv = KvBackend::new();
474        kv.set("plugin-a", b"k".to_vec(), b"v1".to_vec());
475        kv.set("plugin-b", b"k".to_vec(), b"v2".to_vec());
476        assert_eq!(kv.get("plugin-a", b"k"), Some(b"v1".to_vec()));
477        assert_eq!(kv.get("plugin-b", b"k"), Some(b"v2".to_vec()));
478        assert_eq!(kv.get("plugin-c", b"k"), None);
479    }
480
481    #[test]
482    fn kv_overwrite_is_idempotent() {
483        let kv = KvBackend::new();
484        kv.set("p", b"k".to_vec(), b"v1".to_vec());
485        kv.set("p", b"k".to_vec(), b"v2".to_vec());
486        assert_eq!(kv.get("p", b"k"), Some(b"v2".to_vec()));
487        assert_eq!(kv.len("p"), 1);
488    }
489
490    #[test]
491    fn kv_delete_idempotent_on_missing() {
492        let kv = KvBackend::new();
493        kv.delete("p", b"never-set");
494        kv.set("p", b"k".to_vec(), b"v".to_vec());
495        kv.delete("p", b"k");
496        assert_eq!(kv.get("p", b"k"), None);
497    }
498
499    #[test]
500    fn kv_list_keys_empty_namespace_is_empty() {
501        let kv = KvBackend::new();
502        assert!(kv.list_keys("nobody", b"").is_empty());
503    }
504
505    #[test]
506    fn kv_list_keys_filters_by_prefix() {
507        let kv = KvBackend::new();
508        kv.set("p", b"budget/a".to_vec(), b"1".to_vec());
509        kv.set("p", b"budget/b".to_vec(), b"2".to_vec());
510        kv.set("p", b"region_map".to_vec(), b"3".to_vec());
511
512        // No prefix → every key (order-independent).
513        let mut all = kv.list_keys("p", b"");
514        all.sort();
515        assert_eq!(all, vec!["budget/a", "budget/b", "region_map"]);
516
517        // Prefix filter keeps only the matching keys.
518        let mut budget = kv.list_keys("p", b"budget/");
519        budget.sort();
520        assert_eq!(budget, vec!["budget/a", "budget/b"]);
521    }
522
523    #[test]
524    fn kv_value_cap_rejects_oversized_value() {
525        let kv = KvBackend::with_limits(4, 0, 0, 0);
526        // 4 bytes is exactly the cap — allowed.
527        assert!(kv.set("p", b"k".to_vec(), b"1234".to_vec()));
528        // 5 bytes exceeds it — rejected, store unchanged.
529        assert!(!kv.set("p", b"k".to_vec(), b"12345".to_vec()));
530        assert_eq!(kv.get("p", b"k"), Some(b"1234".to_vec()));
531    }
532
533    #[test]
534    fn kv_value_cap_also_bounds_key_length() {
535        let kv = KvBackend::with_limits(4, 0, 0, 0);
536        // A 4-byte key is at the cap — allowed.
537        assert!(kv.set("p", b"kkkk".to_vec(), b"v".to_vec()));
538        // A 5-byte key exceeds the cap — rejected, store unchanged.
539        assert!(!kv.set("p", b"kkkkk".to_vec(), b"v".to_vec()));
540        assert_eq!(kv.get("p", b"kkkkk"), None);
541        assert_eq!(kv.len("p"), 1);
542    }
543
544    #[test]
545    fn kv_namespace_cap_blocks_new_plugins_but_allows_existing() {
546        let kv = KvBackend::with_limits(0, 0, 2, 0);
547        // Two distinct namespaces fit under the cap of 2.
548        assert!(kv.set("a", b"k".to_vec(), b"1".to_vec()));
549        assert!(kv.set("b", b"k".to_vec(), b"2".to_vec()));
550        // A third distinct namespace would exceed it — rejected, and no
551        // empty namespace is left behind.
552        assert!(!kv.set("c", b"k".to_vec(), b"3".to_vec()));
553        assert_eq!(kv.get("c", b"k"), None);
554        assert!(kv.list_keys("c", b"").is_empty());
555        // Writing MORE keys into an already-present namespace is always
556        // allowed — the namespace count stays constant.
557        assert!(kv.set("a", b"k2".to_vec(), b"9".to_vec()));
558        assert_eq!(kv.len("a"), 2);
559    }
560
561    #[test]
562    fn kv_delete_reclaims_empty_namespace_slot() {
563        // With a namespace cap of 1, draining the sole namespace must
564        // free its slot so a different namespace can then be created.
565        let kv = KvBackend::with_limits(0, 0, 1, 0);
566        assert!(kv.set("a", b"k".to_vec(), b"1".to_vec()));
567        // Cap is full — a second namespace is refused.
568        assert!(!kv.set("b", b"k".to_vec(), b"2".to_vec()));
569        // Drain "a"; its now-empty namespace is dropped, freeing a slot.
570        kv.delete("a", b"k");
571        assert_eq!(kv.len("a"), 0);
572        // The reclaimed slot lets a fresh namespace be created.
573        assert!(kv.set("b", b"k".to_vec(), b"2".to_vec()));
574        assert_eq!(kv.get("b", b"k"), Some(b"2".to_vec()));
575    }
576
577    #[test]
578    fn kv_key_count_cap_blocks_new_keys_but_allows_overwrite() {
579        let kv = KvBackend::with_limits(0, 2, 0, 0);
580        assert!(kv.set("p", b"a".to_vec(), b"1".to_vec()));
581        assert!(kv.set("p", b"b".to_vec(), b"2".to_vec()));
582        // Third distinct key would exceed the cap of 2 — rejected.
583        assert!(!kv.set("p", b"c".to_vec(), b"3".to_vec()));
584        assert_eq!(kv.len("p"), 2);
585        // Overwriting an existing key under a full cap still succeeds.
586        assert!(kv.set("p", b"a".to_vec(), b"updated".to_vec()));
587        assert_eq!(kv.get("p", b"a"), Some(b"updated".to_vec()));
588        assert_eq!(kv.len("p"), 2);
589    }
590
591    #[test]
592    fn kv_zero_caps_mean_unlimited() {
593        let kv = KvBackend::with_limits(0, 0, 0, 0);
594        // A large value and many keys both succeed under 0 = unlimited.
595        assert!(kv.set("p", b"big".to_vec(), vec![0u8; 1_000_000]));
596        for i in 0..1000u32 {
597            assert!(kv.set("p", i.to_le_bytes().to_vec(), b"v".to_vec()));
598        }
599        assert_eq!(kv.len("p"), 1001);
600    }
601
602    #[test]
603    fn kv_total_bytes_cap_counts_key_value_and_namespace() {
604        // Cap of 10 total bytes. The first write charges the namespace
605        // name "p" (1) + key "k" (1) + value "abc" (3) = 5 bytes — fits.
606        let kv = KvBackend::with_limits(0, 0, 0, 10);
607        assert!(kv.set("p", b"k".to_vec(), b"abc".to_vec()));
608        // A second key in the SAME namespace: key "k2" (2) + value
609        // "xy" (2) = +4 → total 9 (the namespace name is not recounted).
610        assert!(kv.set("p", b"k2".to_vec(), b"xy".to_vec()));
611        // A further +2 bytes (key "z" + value "q") would reach 11 > 10 —
612        // rejected, and the store is left unchanged.
613        assert!(!kv.set("p", b"z".to_vec(), b"q".to_vec()));
614        assert_eq!(kv.get("p", b"z"), None);
615        assert_eq!(kv.len("p"), 2);
616    }
617
618    #[test]
619    fn kv_total_bytes_cap_charges_overwrite_value_delta_only() {
620        // Cap 8. Namespace "p" (1) + key "k" (1) + value "aa" (2) = 4.
621        let kv = KvBackend::with_limits(0, 0, 0, 8);
622        assert!(kv.set("p", b"k".to_vec(), b"aa".to_vec()));
623        // Overwrite with a 4-byte value: only the value delta (+2) is
624        // charged (key/namespace already counted) → total 6, fits.
625        assert!(kv.set("p", b"k".to_vec(), b"aaaa".to_vec()));
626        assert_eq!(kv.get("p", b"k"), Some(b"aaaa".to_vec()));
627        // Overwrite with a 7-byte value: delta +3 → total 9 > 8 —
628        // rejected, and the previous value survives intact.
629        assert!(!kv.set("p", b"k".to_vec(), b"aaaaaaa".to_vec()));
630        assert_eq!(kv.get("p", b"k"), Some(b"aaaa".to_vec()));
631    }
632
633    #[test]
634    fn kv_total_bytes_cap_reclaimed_on_delete() {
635        // Cap 6: namespace "p" (1) + key "k" (1) + value "aaaa" (4) = 6,
636        // which fills the cap exactly.
637        let kv = KvBackend::with_limits(0, 0, 0, 6);
638        assert!(kv.set("p", b"k".to_vec(), b"aaaa".to_vec()));
639        // No room for anything more.
640        assert!(!kv.set("p", b"k2".to_vec(), b"z".to_vec()));
641        // Deleting the sole key drains the namespace and reclaims all 6
642        // bytes (key + value + namespace name), so a fresh write of the
643        // same size into a different namespace now fits.
644        kv.delete("p", b"k");
645        assert!(kv.set("q", b"k".to_vec(), b"aaaa".to_vec()));
646        assert_eq!(kv.get("q", b"k"), Some(b"aaaa".to_vec()));
647    }
648}