camel_component_wasm/
state_store.rs1use std::collections::HashMap;
2use std::fmt;
3use std::sync::Arc;
4use std::sync::Mutex;
5
6const DEFAULT_MAX_ENTRIES: usize = crate::config::DEFAULT_MAX_KV_ENTRIES;
8
9const DEFAULT_MAX_KEY_BYTES: usize = crate::config::DEFAULT_MAX_KEY_BYTES;
11
12const DEFAULT_MAX_VALUE_BYTES: usize = crate::config::DEFAULT_MAX_VALUE_BYTES;
14
15#[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 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 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 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 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}