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}