Skip to main content

camel_component_wasm/
state_store.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::sync::Arc;
4use std::sync::Mutex;
5
6/// Default maximum number of `StateStore` entries per producer.
7const DEFAULT_MAX_ENTRIES: usize = crate::config::DEFAULT_MAX_KV_ENTRIES;
8
9/// Default maximum byte length of a `StateStore` key.
10const DEFAULT_MAX_KEY_BYTES: usize = crate::config::DEFAULT_MAX_KEY_BYTES;
11
12/// Default maximum byte length of a `StateStore` value (64 KiB).
13const DEFAULT_MAX_VALUE_BYTES: usize = crate::config::DEFAULT_MAX_VALUE_BYTES;
14
15/// Persistent key-value store scoped to a WASM producer (per route endpoint).
16///
17/// Each route endpoint using a WASM component gets its own independent state store.
18/// If two routes use the same `.wasm` file, they maintain separate state.
19/// Owned by `WasmProducer` and passed to `WasmRuntime` when creating host state.
20///
21/// ADR-0051 credential boundary: manual-redaction
22#[derive(Clone)]
23pub struct StateStore {
24    data: Arc<Mutex<HashMap<String, String>>>,
25    max_entries: usize,
26    max_key_bytes: usize,
27    max_value_bytes: usize,
28}
29
30impl fmt::Debug for StateStore {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        f.debug_struct("StateStore")
33            .field("data", &"[REDACTED]")
34            .finish()
35    }
36}
37
38impl StateStore {
39    pub fn new() -> Self {
40        Self::with_limits(
41            DEFAULT_MAX_ENTRIES,
42            DEFAULT_MAX_KEY_BYTES,
43            DEFAULT_MAX_VALUE_BYTES,
44        )
45    }
46
47    /// Build a `StateStore` with explicit bounds on entry count, key length, and
48    /// value length. `store` rejects writes that exceed any of these limits.
49    pub fn with_limits(max_entries: usize, max_key_bytes: usize, max_value_bytes: usize) -> Self {
50        Self {
51            data: Arc::new(Mutex::new(HashMap::new())),
52            max_entries,
53            max_key_bytes,
54            max_value_bytes,
55        }
56    }
57
58    pub fn store(&self, key: &str, value: &str) -> Result<(), String> {
59        if key.len() > self.max_key_bytes {
60            return Err(format!(
61                "key exceeds max_key_bytes limit ({})",
62                self.max_key_bytes
63            ));
64        }
65        if value.len() > self.max_value_bytes {
66            return Err(format!(
67                "value exceeds max_value_bytes limit ({})",
68                self.max_value_bytes
69            ));
70        }
71        let mut guard = self
72            .data
73            .lock()
74            .map_err(|e| format!("lock poisoned: {}", e))?;
75        if !guard.contains_key(key) && guard.len() >= self.max_entries {
76            return Err(format!("kv entry limit exceeded ({})", self.max_entries));
77        }
78        guard.insert(key.to_string(), value.to_string());
79        Ok(())
80    }
81
82    pub fn load(&self, key: &str) -> Result<Option<String>, String> {
83        let guard = self
84            .data
85            .lock()
86            .map_err(|e| format!("lock poisoned: {}", e))?;
87        Ok(guard.get(key).cloned())
88    }
89
90    pub fn is_empty(&self) -> bool {
91        self.len() == 0
92    }
93
94    pub fn len(&self) -> usize {
95        self.data.lock().map(|g| g.len()).unwrap_or(0)
96    }
97
98    pub(crate) fn max_key_bytes(&self) -> usize {
99        self.max_key_bytes
100    }
101
102    pub(crate) fn max_value_bytes(&self) -> usize {
103        self.max_value_bytes
104    }
105}
106
107impl Default for StateStore {
108    fn default() -> Self {
109        Self::new()
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn debug_redacts_secrets() {
119        let store = StateStore::new();
120        store.store("api-key", "SENTINEL-GUEST-SECRET").unwrap();
121        let debug_output = format!("{:?}", store);
122        assert!(
123            !debug_output.contains("SENTINEL-GUEST-SECRET"),
124            "Debug output must not contain secret values: {}",
125            debug_output
126        );
127    }
128
129    #[test]
130    fn test_store_rejects_oversized_key() {
131        let store = StateStore::with_limits(256, 10, 65536);
132        // 22 ASCII chars > 10-byte max_key_bytes cap.
133        let oversized = "a-key-much-longer-than-ten";
134        let err = store.store(oversized, "v").unwrap_err();
135        assert!(
136            err.contains("max_key_bytes"),
137            "expected error to mention max_key_bytes, got: {err}"
138        );
139    }
140
141    #[test]
142    fn test_store_rejects_oversized_value() {
143        let store = StateStore::with_limits(256, 1024, 10);
144        // 30 ASCII chars > 10-byte max_value_bytes cap.
145        let oversized = "this value is far too long!!";
146        let err = store.store("k", oversized).unwrap_err();
147        assert!(
148            err.contains("max_value_bytes"),
149            "expected error to mention max_value_bytes, got: {err}"
150        );
151    }
152
153    #[test]
154    fn test_store_rejects_entry_count_overflow() {
155        let store = StateStore::with_limits(2, 1024, 65536);
156        store.store("k1", "v1").unwrap();
157        store.store("k2", "v2").unwrap();
158        let err = store.store("k3", "v3").unwrap_err();
159        assert!(
160            err.contains("kv entry limit"),
161            "expected error to mention kv entry limit, got: {err}"
162        );
163    }
164
165    #[test]
166    fn test_store_allows_update_within_bounds() {
167        let store = StateStore::with_limits(2, 1024, 65536);
168        store.store("k1", "v1").unwrap();
169        store.store("k2", "v2").unwrap();
170        // Updating an existing key must not count against max_entries.
171        store.store("k1", "v1-updated").unwrap();
172        let loaded = store.load("k1").unwrap();
173        assert_eq!(loaded.as_deref(), Some("v1-updated"));
174        assert_eq!(store.len(), 2);
175    }
176}