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.
22//! - `kv_delete`: `0` (idempotent — no error if the key was absent).
23//!
24//! The implementation is in-process and in-memory. A future slice
25//! can swap the backend for a persistent store (sled, redb, …)
26//! without changing the import surface.
27
28use std::collections::HashMap;
29use std::sync::Arc;
30
31use parking_lot::RwLock;
32use wasmtime::{Caller, Linker, Memory};
33
34use super::runtime::PluginError;
35
36/// KV store type alias: plugin-name -> (key -> value)
37type KvStore = HashMap<String, HashMap<Vec<u8>, Vec<u8>>>;
38
39/// In-memory KV backend, namespaced by plugin name. The outer map
40/// is keyed by plugin name; the inner map by user-supplied key.
41#[derive(Clone, Default)]
42pub struct KvBackend {
43    inner: Arc<RwLock<KvStore>>,
44}
45
46impl KvBackend {
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Read a value. None if missing.
52    pub fn get(&self, plugin: &str, key: &[u8]) -> Option<Vec<u8>> {
53        let g = self.inner.read();
54        g.get(plugin).and_then(|m| m.get(key).cloned())
55    }
56
57    /// Insert / overwrite.
58    pub fn set(&self, plugin: &str, key: Vec<u8>, value: Vec<u8>) {
59        let mut g = self.inner.write();
60        g.entry(plugin.to_string()).or_default().insert(key, value);
61    }
62
63    /// Delete; idempotent.
64    pub fn delete(&self, plugin: &str, key: &[u8]) {
65        let mut g = self.inner.write();
66        if let Some(m) = g.get_mut(plugin) {
67            m.remove(key);
68        }
69    }
70
71    /// Returns the number of keys in the plugin's namespace.
72    /// Useful for tests and the future admin endpoint.
73    pub fn len(&self, plugin: &str) -> usize {
74        self.inner.read().get(plugin).map(|m| m.len()).unwrap_or(0)
75    }
76}
77
78/// Per-call store data: the plugin name (so host imports route to
79/// the right KV namespace) and a clone of the shared KV backend.
80/// Carrying the Arc<KvBackend> by value here is cheap (one atomic
81/// inc) and lets the import functions call `caller.data()` to
82/// retrieve it.
83pub struct StoreCtx {
84    pub plugin_name: String,
85    pub kv: KvBackend,
86}
87
88/// Register all host imports under the `env` module against the
89/// supplied linker. Idempotent — calling twice replaces prior bindings.
90pub fn register_kv_imports(linker: &mut Linker<StoreCtx>) -> Result<(), PluginError> {
91    linker
92        .func_wrap(
93            "env",
94            "kv_get",
95            |mut caller: Caller<'_, StoreCtx>,
96             key_ptr: i32,
97             key_len: i32,
98             val_out_ptr: i32,
99             val_max_len: i32|
100             -> i32 {
101                let memory = match get_memory(&mut caller) {
102                    Some(m) => m,
103                    None => return -1,
104                };
105                let key = match read_bytes(&memory, &caller, key_ptr, key_len) {
106                    Some(b) => b,
107                    None => return -1,
108                };
109                let plugin_name = caller.data().plugin_name.clone();
110                let kv = caller.data().kv.clone();
111                let value = match kv.get(&plugin_name, &key) {
112                    Some(v) => v,
113                    None => return -1,
114                };
115                if (value.len() as i32) > val_max_len {
116                    return -2;
117                }
118                if write_bytes(&memory, &mut caller, val_out_ptr, &value).is_err() {
119                    return -1;
120                }
121                value.len() as i32
122            },
123        )
124        .map_err(|e| PluginError::RuntimeError(format!("link kv_get: {}", e)))?;
125
126    linker
127        .func_wrap(
128            "env",
129            "kv_set",
130            |mut caller: Caller<'_, StoreCtx>,
131             key_ptr: i32,
132             key_len: i32,
133             val_ptr: i32,
134             val_len: i32|
135             -> i32 {
136                let memory = match get_memory(&mut caller) {
137                    Some(m) => m,
138                    None => return -1,
139                };
140                let key = match read_bytes(&memory, &caller, key_ptr, key_len) {
141                    Some(b) => b,
142                    None => return -1,
143                };
144                let val = match read_bytes(&memory, &caller, val_ptr, val_len) {
145                    Some(b) => b,
146                    None => return -1,
147                };
148                let plugin_name = caller.data().plugin_name.clone();
149                let kv = caller.data().kv.clone();
150                kv.set(&plugin_name, key, val);
151                0
152            },
153        )
154        .map_err(|e| PluginError::RuntimeError(format!("link kv_set: {}", e)))?;
155
156    linker
157        .func_wrap(
158            "env",
159            "kv_delete",
160            |mut caller: Caller<'_, StoreCtx>, key_ptr: i32, key_len: i32| -> i32 {
161                let memory = match get_memory(&mut caller) {
162                    Some(m) => m,
163                    None => return -1,
164                };
165                let key = match read_bytes(&memory, &caller, key_ptr, key_len) {
166                    Some(b) => b,
167                    None => return -1,
168                };
169                let plugin_name = caller.data().plugin_name.clone();
170                let kv = caller.data().kv.clone();
171                kv.delete(&plugin_name, &key);
172                0
173            },
174        )
175        .map_err(|e| PluginError::RuntimeError(format!("link kv_delete: {}", e)))?;
176
177    Ok(())
178}
179
180/// Register the `env.sha256_hex` host import. Plugins call:
181///
182/// ```text
183/// env.sha256_hex(in_ptr: i32, in_len: i32, out_ptr: i32) -> i32
184/// ```
185///
186/// where `out_ptr` must point to at least 64 bytes inside plugin
187/// memory (the lower-case hex SHA-256 digest is exactly 64 ASCII
188/// chars). Returns 64 on success, -1 on memory error.
189///
190/// The host computes the digest over the plugin-supplied byte range
191/// using the production `sha2` crate; plugins no longer need to
192/// embed their own (placeholder) hash and stay small.
193pub fn register_crypto_imports(linker: &mut Linker<StoreCtx>) -> Result<(), PluginError> {
194    use sha2::{Digest, Sha256};
195
196    linker
197        .func_wrap(
198            "env",
199            "sha256_hex",
200            |mut caller: Caller<'_, StoreCtx>, in_ptr: i32, in_len: i32, out_ptr: i32| -> i32 {
201                let memory = match get_memory(&mut caller) {
202                    Some(m) => m,
203                    None => return -1,
204                };
205                let input = match read_bytes(&memory, &caller, in_ptr, in_len) {
206                    Some(b) => b,
207                    None => return -1,
208                };
209                let digest = Sha256::digest(&input);
210                // Hex-encode into a fixed 64-byte stack buffer so we
211                // don't allocate per call.
212                let mut hex = [0u8; 64];
213                const HEX: &[u8; 16] = b"0123456789abcdef";
214                for (i, b) in digest.iter().enumerate() {
215                    hex[i * 2] = HEX[(b >> 4) as usize];
216                    hex[i * 2 + 1] = HEX[(b & 0x0f) as usize];
217                }
218                if write_bytes(&memory, &mut caller, out_ptr, &hex).is_err() {
219                    return -1;
220                }
221                64
222            },
223        )
224        .map_err(|e| PluginError::RuntimeError(format!("link sha256_hex: {}", e)))?;
225    Ok(())
226}
227
228fn get_memory(caller: &mut Caller<'_, StoreCtx>) -> Option<Memory> {
229    caller.get_export("memory").and_then(|e| e.into_memory())
230}
231
232fn read_bytes(
233    memory: &Memory,
234    caller: &Caller<'_, StoreCtx>,
235    ptr: i32,
236    len: i32,
237) -> Option<Vec<u8>> {
238    if len < 0 {
239        return None;
240    }
241    let start = ptr as usize;
242    let end = start.checked_add(len as usize)?;
243    let data = memory.data(caller);
244    data.get(start..end).map(|s| s.to_vec())
245}
246
247fn write_bytes(
248    memory: &Memory,
249    caller: &mut Caller<'_, StoreCtx>,
250    ptr: i32,
251    bytes: &[u8],
252) -> Result<(), ()> {
253    let start = ptr as usize;
254    let end = start.checked_add(bytes.len()).ok_or(())?;
255    let data = memory.data_mut(caller);
256    let slot = data.get_mut(start..end).ok_or(())?;
257    slot.copy_from_slice(bytes);
258    Ok(())
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn kv_namespaced_per_plugin() {
267        let kv = KvBackend::new();
268        kv.set("plugin-a", b"k".to_vec(), b"v1".to_vec());
269        kv.set("plugin-b", b"k".to_vec(), b"v2".to_vec());
270        assert_eq!(kv.get("plugin-a", b"k"), Some(b"v1".to_vec()));
271        assert_eq!(kv.get("plugin-b", b"k"), Some(b"v2".to_vec()));
272        assert_eq!(kv.get("plugin-c", b"k"), None);
273    }
274
275    #[test]
276    fn kv_overwrite_is_idempotent() {
277        let kv = KvBackend::new();
278        kv.set("p", b"k".to_vec(), b"v1".to_vec());
279        kv.set("p", b"k".to_vec(), b"v2".to_vec());
280        assert_eq!(kv.get("p", b"k"), Some(b"v2".to_vec()));
281        assert_eq!(kv.len("p"), 1);
282    }
283
284    #[test]
285    fn kv_delete_idempotent_on_missing() {
286        let kv = KvBackend::new();
287        kv.delete("p", b"never-set");
288        kv.set("p", b"k".to_vec(), b"v".to_vec());
289        kv.delete("p", b"k");
290        assert_eq!(kv.get("p", b"k"), None);
291    }
292}