Skip to main content

cdk_common/database/
kvstore.rs

1//! Key-Value Store Database traits and utilities
2//!
3//! This module provides shared KVStore functionality that can be used by both
4//! mint and wallet database implementations.
5
6use async_trait::async_trait;
7
8use super::{DbTransactionFinalizer, Error};
9
10/// Valid ASCII characters for namespace and key strings in KV store
11pub const KVSTORE_NAMESPACE_KEY_ALPHABET: &str =
12    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
13
14/// Maximum length for namespace and key strings in KV store
15pub const KVSTORE_NAMESPACE_KEY_MAX_LEN: usize = 120;
16
17/// Validates that a string contains only valid KV store characters and is within length limits
18pub 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
35/// Validates namespace and key parameters for KV store operations
36pub fn validate_kvstore_params(
37    primary_namespace: &str,
38    secondary_namespace: &str,
39    key: Option<&str>,
40) -> Result<(), Error> {
41    // Validate primary namespace
42    validate_kvstore_string(primary_namespace)?;
43
44    // Validate secondary namespace
45    validate_kvstore_string(secondary_namespace)?;
46
47    // Check empty namespace rules
48    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 key
56        validate_kvstore_string(key)?;
57
58        // Check for potential collisions between keys and namespaces in the same namespace
59        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/// Key-Value Store Transaction trait
71#[async_trait]
72pub trait KVStoreTransaction<Error>: DbTransactionFinalizer<Err = Error> {
73    /// Read value from key-value store
74    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    /// Write value to key-value store
82    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    /// Remove value from key-value store
91    async fn kv_remove(
92        &mut self,
93        primary_namespace: &str,
94        secondary_namespace: &str,
95        key: &str,
96    ) -> Result<(), Error>;
97
98    /// List keys in a namespace
99    async fn kv_list(
100        &mut self,
101        primary_namespace: &str,
102        secondary_namespace: &str,
103    ) -> Result<Vec<String>, Error>;
104}
105
106/// Key-Value Store Database trait
107#[async_trait]
108pub trait KVStoreDatabase {
109    /// KV Store Database Error
110    type Err: Into<Error> + From<Error>;
111
112    /// Read value from key-value store
113    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    /// List keys in a namespace
121    async fn kv_list(
122        &self,
123        primary_namespace: &str,
124        secondary_namespace: &str,
125    ) -> Result<Vec<String>, Self::Err>;
126}
127
128/// Key-Value Store trait combining read operations with transaction support
129#[async_trait]
130pub trait KVStore: KVStoreDatabase {
131    /// Begins a KV transaction
132    async fn begin_transaction(
133        &self,
134    ) -> Result<Box<dyn KVStoreTransaction<Self::Err> + Send + Sync>, Error>;
135}
136
137#[cfg(test)]
138mod tests {
139    use super::{
140        validate_kvstore_params, validate_kvstore_string, KVSTORE_NAMESPACE_KEY_ALPHABET,
141        KVSTORE_NAMESPACE_KEY_MAX_LEN,
142    };
143
144    #[test]
145    fn test_validate_kvstore_string_valid_inputs() {
146        // Test valid strings
147        assert!(validate_kvstore_string("").is_ok());
148        assert!(validate_kvstore_string("abc").is_ok());
149        assert!(validate_kvstore_string("ABC").is_ok());
150        assert!(validate_kvstore_string("123").is_ok());
151        assert!(validate_kvstore_string("test_key").is_ok());
152        assert!(validate_kvstore_string("test-key").is_ok());
153        assert!(validate_kvstore_string("test_KEY-123").is_ok());
154
155        // Test max length string
156        let max_length_str = "a".repeat(KVSTORE_NAMESPACE_KEY_MAX_LEN);
157        assert!(validate_kvstore_string(&max_length_str).is_ok());
158    }
159
160    #[test]
161    fn test_validate_kvstore_string_invalid_length() {
162        // Test string too long
163        let too_long_str = "a".repeat(KVSTORE_NAMESPACE_KEY_MAX_LEN + 1);
164        let result = validate_kvstore_string(&too_long_str);
165        assert!(result.is_err());
166        assert!(result
167            .unwrap_err()
168            .to_string()
169            .contains("exceeds maximum length"));
170    }
171
172    #[test]
173    fn test_validate_kvstore_string_invalid_characters() {
174        // Test invalid characters
175        let invalid_chars = vec![
176            "test@key",  // @
177            "test key",  // space
178            "test.key",  // .
179            "test/key",  // /
180            "test\\key", // \
181            "test+key",  // +
182            "test=key",  // =
183            "test!key",  // !
184            "test#key",  // #
185            "test$key",  // $
186            "test%key",  // %
187            "test&key",  // &
188            "test*key",  // *
189            "test(key",  // (
190            "test)key",  // )
191            "test[key",  // [
192            "test]key",  // ]
193            "test{key",  // {
194            "test}key",  // }
195            "test|key",  // |
196            "test;key",  // ;
197            "test:key",  // :
198            "test'key",  // '
199            "test\"key", // "
200            "test<key",  // <
201            "test>key",  // >
202            "test,key",  // ,
203            "test?key",  // ?
204            "test~key",  // ~
205            "test`key",  // `
206        ];
207
208        for invalid_str in invalid_chars {
209            let result = validate_kvstore_string(invalid_str);
210            assert!(result.is_err(), "Expected '{}' to be invalid", invalid_str);
211            assert!(result
212                .unwrap_err()
213                .to_string()
214                .contains("invalid characters"));
215        }
216    }
217
218    #[test]
219    fn test_validate_kvstore_params_valid() {
220        // Test valid parameter combinations
221        assert!(validate_kvstore_params("primary", "secondary", Some("key")).is_ok());
222        assert!(validate_kvstore_params("primary", "", Some("key")).is_ok());
223        assert!(validate_kvstore_params("", "", Some("key")).is_ok());
224        assert!(validate_kvstore_params("p1", "s1", Some("different_key")).is_ok());
225    }
226
227    #[test]
228    fn test_validate_kvstore_params_empty_namespace_rules() {
229        // Test empty namespace rules: if primary is empty, secondary must be empty too
230        let result = validate_kvstore_params("", "secondary", Some("key"));
231        assert!(result.is_err());
232        assert!(result
233            .unwrap_err()
234            .to_string()
235            .contains("If primary_namespace is empty"));
236    }
237
238    #[test]
239    fn test_validate_kvstore_params_collision_prevention() {
240        // Test collision prevention between keys and namespaces
241        let test_cases = vec![
242            ("primary", "secondary", "primary"), // key matches primary namespace
243            ("primary", "secondary", "secondary"), // key matches secondary namespace
244        ];
245
246        for (primary, secondary, key) in test_cases {
247            let result = validate_kvstore_params(primary, secondary, Some(key));
248            assert!(
249                result.is_err(),
250                "Expected collision for key '{}' with namespaces '{}'/'{}'",
251                key,
252                primary,
253                secondary
254            );
255            let error_msg = result.unwrap_err().to_string();
256            assert!(error_msg.contains("conflicts with namespace"));
257        }
258
259        // Test that a combined namespace string would be invalid due to the slash character
260        let result = validate_kvstore_params("primary", "secondary", Some("primary_secondary"));
261        assert!(result.is_ok(), "This should be valid - no actual collision");
262    }
263
264    #[test]
265    fn test_validate_kvstore_params_invalid_strings() {
266        // Test invalid characters in any parameter
267        let result = validate_kvstore_params("primary@", "secondary", Some("key"));
268        assert!(result.is_err());
269
270        let result = validate_kvstore_params("primary", "secondary!", Some("key"));
271        assert!(result.is_err());
272
273        let result = validate_kvstore_params("primary", "secondary", Some("key with space"));
274        assert!(result.is_err());
275    }
276
277    #[test]
278    fn test_alphabet_constants() {
279        // Verify the alphabet constant is as expected
280        assert_eq!(
281            KVSTORE_NAMESPACE_KEY_ALPHABET,
282            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
283        );
284        assert_eq!(KVSTORE_NAMESPACE_KEY_MAX_LEN, 120);
285    }
286
287    #[test]
288    fn test_alphabet_coverage() {
289        // Test that all valid characters are actually accepted
290        for ch in KVSTORE_NAMESPACE_KEY_ALPHABET.chars() {
291            let test_str = ch.to_string();
292            assert!(
293                validate_kvstore_string(&test_str).is_ok(),
294                "Character '{}' should be valid",
295                ch
296            );
297        }
298    }
299
300    #[test]
301    fn test_namespace_segmentation_examples() {
302        // Test realistic namespace segmentation scenarios
303
304        // Valid segmentation examples
305        let valid_examples = vec![
306            ("wallets", "user123", "balance"),
307            ("quotes", "mint", "quote_12345"),
308            ("keysets", "", "active_keyset"),
309            ("", "", "global_config"),
310            ("auth", "session_456", "token"),
311            ("mint_info", "", "version"),
312        ];
313
314        for (primary, secondary, key) in valid_examples {
315            assert!(
316                validate_kvstore_params(primary, secondary, Some(key)).is_ok(),
317                "Valid example should pass: '{}'/'{}'/'{}'",
318                primary,
319                secondary,
320                key
321            );
322        }
323    }
324
325    #[test]
326    fn test_per_namespace_uniqueness() {
327        // This test documents the requirement that implementations should ensure
328        // per-namespace key uniqueness. The validation function doesn't enforce
329        // database-level uniqueness (that's handled by the database schema),
330        // but ensures naming conflicts don't occur between keys and namespaces.
331
332        // These should be valid (different namespaces)
333        assert!(validate_kvstore_params("ns1", "sub1", Some("key1")).is_ok());
334        assert!(validate_kvstore_params("ns2", "sub1", Some("key1")).is_ok()); // same key, different primary namespace
335        assert!(validate_kvstore_params("ns1", "sub2", Some("key1")).is_ok()); // same key, different secondary namespace
336
337        // These should fail (collision within namespace)
338        assert!(validate_kvstore_params("ns1", "sub1", Some("ns1")).is_err()); // key conflicts with primary namespace
339        assert!(validate_kvstore_params("ns1", "sub1", Some("sub1")).is_err()); // key conflicts with secondary namespace
340    }
341}