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 capability for atomic compare-and-swap operations.
129///
130/// This is a separate capability because not every [`KVStoreDatabase`]
131/// backend can guarantee an atomic conditional write.
132#[async_trait]
133pub trait KVStoreCompareAndSwap: KVStoreDatabase {
134    /// Replaces a value only when its current value matches `expected`.
135    ///
136    /// An `expected` value of `None` inserts only when the key does not exist.
137    /// Returns `true` when the value was changed and `false` when the
138    /// expectation did not match.
139    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/// Key-Value Store trait combining read operations with transaction support
150#[async_trait]
151pub trait KVStore: KVStoreDatabase {
152    /// Begins a KV transaction
153    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        // Test valid strings
168        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        // Test max length string
177        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        // Test string too long
184        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        // Test invalid characters
196        let invalid_chars = vec![
197            "test@key",  // @
198            "test key",  // space
199            "test.key",  // .
200            "test/key",  // /
201            "test\\key", // \
202            "test+key",  // +
203            "test=key",  // =
204            "test!key",  // !
205            "test#key",  // #
206            "test$key",  // $
207            "test%key",  // %
208            "test&key",  // &
209            "test*key",  // *
210            "test(key",  // (
211            "test)key",  // )
212            "test[key",  // [
213            "test]key",  // ]
214            "test{key",  // {
215            "test}key",  // }
216            "test|key",  // |
217            "test;key",  // ;
218            "test:key",  // :
219            "test'key",  // '
220            "test\"key", // "
221            "test<key",  // <
222            "test>key",  // >
223            "test,key",  // ,
224            "test?key",  // ?
225            "test~key",  // ~
226            "test`key",  // `
227        ];
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        // Test valid parameter combinations
242        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        // Test empty namespace rules: if primary is empty, secondary must be empty too
251        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        // Test collision prevention between keys and namespaces
262        let test_cases = vec![
263            ("primary", "secondary", "primary"), // key matches primary namespace
264            ("primary", "secondary", "secondary"), // key matches secondary namespace
265        ];
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        // Test that a combined namespace string would be invalid due to the slash character
281        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        // Test invalid characters in any parameter
288        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        // Verify the alphabet constant is as expected
301        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        // Test that all valid characters are actually accepted
311        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        // Test realistic namespace segmentation scenarios
324
325        // Valid segmentation examples
326        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        // This test documents the requirement that implementations should ensure
349        // per-namespace key uniqueness. The validation function doesn't enforce
350        // database-level uniqueness (that's handled by the database schema),
351        // but ensures naming conflicts don't occur between keys and namespaces.
352
353        // These should be valid (different namespaces)
354        assert!(validate_kvstore_params("ns1", "sub1", Some("key1")).is_ok());
355        assert!(validate_kvstore_params("ns2", "sub1", Some("key1")).is_ok()); // same key, different primary namespace
356        assert!(validate_kvstore_params("ns1", "sub2", Some("key1")).is_ok()); // same key, different secondary namespace
357
358        // These should fail (collision within namespace)
359        assert!(validate_kvstore_params("ns1", "sub1", Some("ns1")).is_err()); // key conflicts with primary namespace
360        assert!(validate_kvstore_params("ns1", "sub1", Some("sub1")).is_err()); // key conflicts with secondary namespace
361    }
362}