1use async_trait::async_trait;
7
8use super::{DbTransactionFinalizer, Error};
9
10pub const KVSTORE_NAMESPACE_KEY_ALPHABET: &str =
12 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
13
14pub const KVSTORE_NAMESPACE_KEY_MAX_LEN: usize = 120;
16
17pub fn validate_kvstore_string(s: &str) -> Result<(), Error> {
19 if s.len() > KVSTORE_NAMESPACE_KEY_MAX_LEN {
20 return Err(Error::KVStoreInvalidKey(format!(
21 "{KVSTORE_NAMESPACE_KEY_MAX_LEN} exceeds maximum length of key characters"
22 )));
23 }
24
25 if !s
26 .chars()
27 .all(|c| KVSTORE_NAMESPACE_KEY_ALPHABET.contains(c))
28 {
29 return Err(Error::KVStoreInvalidKey("key contains invalid characters. Only ASCII letters, numbers, underscore, and hyphen are allowed".to_string()));
30 }
31
32 Ok(())
33}
34
35pub fn validate_kvstore_params(
37 primary_namespace: &str,
38 secondary_namespace: &str,
39 key: Option<&str>,
40) -> Result<(), Error> {
41 validate_kvstore_string(primary_namespace)?;
43
44 validate_kvstore_string(secondary_namespace)?;
46
47 if primary_namespace.is_empty() && !secondary_namespace.is_empty() {
49 return Err(Error::KVStoreInvalidKey(
50 "If primary_namespace is empty, secondary_namespace must also be empty".to_string(),
51 ));
52 }
53
54 if let Some(key) = key {
55 validate_kvstore_string(key)?;
57
58 let namespace_key = format!("{primary_namespace}/{secondary_namespace}");
60 if key == primary_namespace || key == secondary_namespace || key == namespace_key {
61 return Err(Error::KVStoreInvalidKey(format!(
62 "Key '{key}' conflicts with namespace names"
63 )));
64 }
65 }
66
67 Ok(())
68}
69
70#[async_trait]
72pub trait KVStoreTransaction<Error>: DbTransactionFinalizer<Err = Error> {
73 async fn kv_read(
75 &mut self,
76 primary_namespace: &str,
77 secondary_namespace: &str,
78 key: &str,
79 ) -> Result<Option<Vec<u8>>, Error>;
80
81 async fn kv_write(
83 &mut self,
84 primary_namespace: &str,
85 secondary_namespace: &str,
86 key: &str,
87 value: &[u8],
88 ) -> Result<(), Error>;
89
90 async fn kv_remove(
92 &mut self,
93 primary_namespace: &str,
94 secondary_namespace: &str,
95 key: &str,
96 ) -> Result<(), Error>;
97
98 async fn kv_list(
100 &mut self,
101 primary_namespace: &str,
102 secondary_namespace: &str,
103 ) -> Result<Vec<String>, Error>;
104}
105
106#[async_trait]
108pub trait KVStoreDatabase {
109 type Err: Into<Error> + From<Error>;
111
112 async fn kv_read(
114 &self,
115 primary_namespace: &str,
116 secondary_namespace: &str,
117 key: &str,
118 ) -> Result<Option<Vec<u8>>, Self::Err>;
119
120 async fn kv_list(
122 &self,
123 primary_namespace: &str,
124 secondary_namespace: &str,
125 ) -> Result<Vec<String>, Self::Err>;
126}
127
128#[async_trait]
133pub trait KVStoreCompareAndSwap: KVStoreDatabase {
134 async fn kv_compare_and_swap(
140 &self,
141 primary_namespace: &str,
142 secondary_namespace: &str,
143 key: &str,
144 expected: Option<&[u8]>,
145 replacement: &[u8],
146 ) -> Result<bool, Self::Err>;
147}
148
149#[async_trait]
151pub trait KVStore: KVStoreDatabase {
152 async fn begin_transaction(
154 &self,
155 ) -> Result<Box<dyn KVStoreTransaction<Self::Err> + Send + Sync>, Error>;
156}
157
158#[cfg(test)]
159mod tests {
160 use super::{
161 validate_kvstore_params, validate_kvstore_string, KVSTORE_NAMESPACE_KEY_ALPHABET,
162 KVSTORE_NAMESPACE_KEY_MAX_LEN,
163 };
164
165 #[test]
166 fn test_validate_kvstore_string_valid_inputs() {
167 assert!(validate_kvstore_string("").is_ok());
169 assert!(validate_kvstore_string("abc").is_ok());
170 assert!(validate_kvstore_string("ABC").is_ok());
171 assert!(validate_kvstore_string("123").is_ok());
172 assert!(validate_kvstore_string("test_key").is_ok());
173 assert!(validate_kvstore_string("test-key").is_ok());
174 assert!(validate_kvstore_string("test_KEY-123").is_ok());
175
176 let max_length_str = "a".repeat(KVSTORE_NAMESPACE_KEY_MAX_LEN);
178 assert!(validate_kvstore_string(&max_length_str).is_ok());
179 }
180
181 #[test]
182 fn test_validate_kvstore_string_invalid_length() {
183 let too_long_str = "a".repeat(KVSTORE_NAMESPACE_KEY_MAX_LEN + 1);
185 let result = validate_kvstore_string(&too_long_str);
186 assert!(result.is_err());
187 assert!(result
188 .unwrap_err()
189 .to_string()
190 .contains("exceeds maximum length"));
191 }
192
193 #[test]
194 fn test_validate_kvstore_string_invalid_characters() {
195 let invalid_chars = vec![
197 "test@key", "test key", "test.key", "test/key", "test\\key", "test+key", "test=key", "test!key", "test#key", "test$key", "test%key", "test&key", "test*key", "test(key", "test)key", "test[key", "test]key", "test{key", "test}key", "test|key", "test;key", "test:key", "test'key", "test\"key", "test<key", "test>key", "test,key", "test?key", "test~key", "test`key", ];
228
229 for invalid_str in invalid_chars {
230 let result = validate_kvstore_string(invalid_str);
231 assert!(result.is_err(), "Expected '{}' to be invalid", invalid_str);
232 assert!(result
233 .unwrap_err()
234 .to_string()
235 .contains("invalid characters"));
236 }
237 }
238
239 #[test]
240 fn test_validate_kvstore_params_valid() {
241 assert!(validate_kvstore_params("primary", "secondary", Some("key")).is_ok());
243 assert!(validate_kvstore_params("primary", "", Some("key")).is_ok());
244 assert!(validate_kvstore_params("", "", Some("key")).is_ok());
245 assert!(validate_kvstore_params("p1", "s1", Some("different_key")).is_ok());
246 }
247
248 #[test]
249 fn test_validate_kvstore_params_empty_namespace_rules() {
250 let result = validate_kvstore_params("", "secondary", Some("key"));
252 assert!(result.is_err());
253 assert!(result
254 .unwrap_err()
255 .to_string()
256 .contains("If primary_namespace is empty"));
257 }
258
259 #[test]
260 fn test_validate_kvstore_params_collision_prevention() {
261 let test_cases = vec![
263 ("primary", "secondary", "primary"), ("primary", "secondary", "secondary"), ];
266
267 for (primary, secondary, key) in test_cases {
268 let result = validate_kvstore_params(primary, secondary, Some(key));
269 assert!(
270 result.is_err(),
271 "Expected collision for key '{}' with namespaces '{}'/'{}'",
272 key,
273 primary,
274 secondary
275 );
276 let error_msg = result.unwrap_err().to_string();
277 assert!(error_msg.contains("conflicts with namespace"));
278 }
279
280 let result = validate_kvstore_params("primary", "secondary", Some("primary_secondary"));
282 assert!(result.is_ok(), "This should be valid - no actual collision");
283 }
284
285 #[test]
286 fn test_validate_kvstore_params_invalid_strings() {
287 let result = validate_kvstore_params("primary@", "secondary", Some("key"));
289 assert!(result.is_err());
290
291 let result = validate_kvstore_params("primary", "secondary!", Some("key"));
292 assert!(result.is_err());
293
294 let result = validate_kvstore_params("primary", "secondary", Some("key with space"));
295 assert!(result.is_err());
296 }
297
298 #[test]
299 fn test_alphabet_constants() {
300 assert_eq!(
302 KVSTORE_NAMESPACE_KEY_ALPHABET,
303 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
304 );
305 assert_eq!(KVSTORE_NAMESPACE_KEY_MAX_LEN, 120);
306 }
307
308 #[test]
309 fn test_alphabet_coverage() {
310 for ch in KVSTORE_NAMESPACE_KEY_ALPHABET.chars() {
312 let test_str = ch.to_string();
313 assert!(
314 validate_kvstore_string(&test_str).is_ok(),
315 "Character '{}' should be valid",
316 ch
317 );
318 }
319 }
320
321 #[test]
322 fn test_namespace_segmentation_examples() {
323 let valid_examples = vec![
327 ("wallets", "user123", "balance"),
328 ("quotes", "mint", "quote_12345"),
329 ("keysets", "", "active_keyset"),
330 ("", "", "global_config"),
331 ("auth", "session_456", "token"),
332 ("mint_info", "", "version"),
333 ];
334
335 for (primary, secondary, key) in valid_examples {
336 assert!(
337 validate_kvstore_params(primary, secondary, Some(key)).is_ok(),
338 "Valid example should pass: '{}'/'{}'/'{}'",
339 primary,
340 secondary,
341 key
342 );
343 }
344 }
345
346 #[test]
347 fn test_per_namespace_uniqueness() {
348 assert!(validate_kvstore_params("ns1", "sub1", Some("key1")).is_ok());
355 assert!(validate_kvstore_params("ns2", "sub1", Some("key1")).is_ok()); assert!(validate_kvstore_params("ns1", "sub2", Some("key1")).is_ok()); assert!(validate_kvstore_params("ns1", "sub1", Some("ns1")).is_err()); assert!(validate_kvstore_params("ns1", "sub1", Some("sub1")).is_err()); }
362}