1use std::collections::HashMap;
2use std::time::Duration;
3
4use crate::config::StoreConfig;
5use crate::error::AppError;
6use fjall::{KeyspaceCreateOptions, PersistMode};
7use serde::Serialize;
8use serde::de::DeserializeOwned;
9use tracing::info;
10
11pub mod counter;
12
13#[cfg(feature = "encryption")]
14pub(crate) mod encryption;
15
16#[cfg(feature = "vsock-store")]
17pub mod vsock;
18
19const STORE_OP_TIMEOUT: Duration = Duration::from_secs(30);
22
23async fn blocking_with_timeout<F, T>(f: F) -> Result<T, AppError>
25where
26 F: FnOnce() -> Result<T, AppError> + Send + 'static,
27 T: Send + 'static,
28{
29 match tokio::time::timeout(STORE_OP_TIMEOUT, tokio::task::spawn_blocking(f)).await {
30 Ok(Ok(result)) => result,
31 Ok(Err(e)) => Err(AppError::Internal(format!("blocking task panicked: {e}"))),
32 Err(_) => Err(AppError::Internal(format!(
33 "store operation timed out after {}s",
34 STORE_OP_TIMEOUT.as_secs()
35 ))),
36 }
37}
38
39pub type RawKvPair = (Vec<u8>, Vec<u8>);
41
42#[derive(Clone)]
51pub enum Store {
52 Local(LocalStore),
54 #[cfg(feature = "vsock-store")]
56 Vsock(vsock::VsockStore),
57}
58
59impl Store {
60 pub fn open(config: &StoreConfig) -> Result<Self, AppError> {
62 Ok(Store::Local(LocalStore::open(config)?))
63 }
64
65 #[cfg(feature = "vsock-store")]
67 pub async fn connect_vsock(port: Option<u32>) -> Result<Self, AppError> {
68 Ok(Store::Vsock(vsock::VsockStore::connect(port).await?))
69 }
70
71 pub fn keyspace(&self, name: &str) -> Result<KeyspaceHandle, AppError> {
72 match self {
73 Store::Local(s) => Ok(KeyspaceHandle::Local(s.keyspace(name)?)),
74 #[cfg(feature = "vsock-store")]
75 Store::Vsock(s) => Ok(KeyspaceHandle::Vsock(s.keyspace(name)?)),
76 }
77 }
78
79 pub async fn persist(&self) -> Result<(), AppError> {
80 match self {
81 Store::Local(s) => s.persist().await,
82 #[cfg(feature = "vsock-store")]
83 Store::Vsock(s) => s.persist().await,
84 }
85 }
86}
87
88#[derive(Clone)]
97pub enum KeyspaceHandle {
98 Local(LocalKeyspaceHandle),
99 #[cfg(feature = "vsock-store")]
100 Vsock(vsock::VsockKeyspaceHandle),
101}
102
103impl KeyspaceHandle {
104 #[cfg(feature = "encryption")]
105 pub fn with_encryption(self, key: [u8; 32]) -> Self {
106 match self {
107 KeyspaceHandle::Local(h) => KeyspaceHandle::Local(h.with_encryption(key)),
108 #[cfg(feature = "vsock-store")]
109 KeyspaceHandle::Vsock(h) => KeyspaceHandle::Vsock(h.with_encryption(key)),
110 }
111 }
112
113 pub fn is_encrypted(&self) -> bool {
114 match self {
115 KeyspaceHandle::Local(h) => h.is_encrypted(),
116 #[cfg(feature = "vsock-store")]
117 KeyspaceHandle::Vsock(h) => h.is_encrypted(),
118 }
119 }
120
121 #[cfg(feature = "encryption")]
142 pub async fn migrate_to_encrypted(&self, key: [u8; 32]) -> Result<usize, AppError> {
143 if self.is_encrypted() {
144 return Err(AppError::Internal(
145 "migrate_to_encrypted must be called on a bare (unencrypted) keyspace handle"
146 .into(),
147 ));
148 }
149 let rows = self.prefix_iter_raw(Vec::<u8>::new()).await?;
153 let encrypted = self.clone().with_encryption(key);
154 let mut migrated = 0usize;
155 for (k, v) in rows {
156 if encryption::is_v1_encrypted(&v) {
157 continue;
158 }
159 encrypted.insert_raw(k, v).await?;
162 migrated += 1;
163 }
164 Ok(migrated)
165 }
166
167 pub async fn persist(&self) -> Result<(), AppError> {
176 match self {
177 KeyspaceHandle::Local(h) => h.persist().await,
178 #[cfg(feature = "vsock-store")]
179 KeyspaceHandle::Vsock(h) => h.persist().await,
180 }
181 }
182
183 pub async fn insert<V: Serialize>(
184 &self,
185 key: impl Into<Vec<u8>>,
186 value: &V,
187 ) -> Result<(), AppError> {
188 match self {
189 KeyspaceHandle::Local(h) => h.insert(key, value).await,
190 #[cfg(feature = "vsock-store")]
191 KeyspaceHandle::Vsock(h) => h.insert(key, value).await,
192 }
193 }
194
195 pub async fn insert_if_absent<V: Serialize>(
209 &self,
210 key: impl Into<Vec<u8>>,
211 value: &V,
212 ) -> Result<bool, AppError> {
213 match self {
214 KeyspaceHandle::Local(h) => h.insert_if_absent(key, value).await,
215 #[cfg(feature = "vsock-store")]
216 KeyspaceHandle::Vsock(h) => {
217 tracing::warn!(
218 "KeyspaceHandle::Vsock::insert_if_absent using non-atomic get+insert \
219 fallback; vsock proto lacks a native insert-if-absent opcode. \
220 Single-replica TEE deployments are unaffected in practice."
221 );
222 let key = key.into();
223 if h.get_raw(key.clone()).await?.is_some() {
224 return Ok(false);
225 }
226 h.insert(key, value).await?;
227 Ok(true)
228 }
229 }
230 }
231
232 pub async fn insert_raw_if_absent(
236 &self,
237 key: impl Into<Vec<u8>>,
238 value: impl Into<Vec<u8>>,
239 ) -> Result<bool, AppError> {
240 match self {
241 KeyspaceHandle::Local(h) => h.insert_raw_if_absent(key, value).await,
242 #[cfg(feature = "vsock-store")]
243 KeyspaceHandle::Vsock(h) => {
244 tracing::warn!(
245 "KeyspaceHandle::Vsock::insert_raw_if_absent using non-atomic get+insert \
246 fallback; vsock proto lacks a native insert-if-absent opcode. \
247 Single-replica TEE deployments are unaffected in practice."
248 );
249 let key = key.into();
250 if h.get_raw(key.clone()).await?.is_some() {
251 return Ok(false);
252 }
253 h.insert_raw(key, value).await?;
254 Ok(true)
255 }
256 }
257 }
258
259 pub async fn get<V: DeserializeOwned + Send + 'static>(
260 &self,
261 key: impl Into<Vec<u8>>,
262 ) -> Result<Option<V>, AppError> {
263 match self {
264 KeyspaceHandle::Local(h) => h.get(key).await,
265 #[cfg(feature = "vsock-store")]
266 KeyspaceHandle::Vsock(h) => h.get(key).await,
267 }
268 }
269
270 pub async fn remove(&self, key: impl Into<Vec<u8>>) -> Result<(), AppError> {
271 match self {
272 KeyspaceHandle::Local(h) => h.remove(key).await,
273 #[cfg(feature = "vsock-store")]
274 KeyspaceHandle::Vsock(h) => h.remove(key).await,
275 }
276 }
277
278 pub async fn take_raw(&self, key: impl Into<Vec<u8>>) -> Result<Option<Vec<u8>>, AppError> {
291 let key = key.into();
292 match self {
293 KeyspaceHandle::Local(h) => h.take_raw(key).await,
294 #[cfg(feature = "vsock-store")]
295 KeyspaceHandle::Vsock(h) => {
296 tracing::warn!(
297 "KeyspaceHandle::Vsock::take_raw using non-atomic get+remove fallback; \
298 vsock proto lacks a native take opcode. Single-replica TEE deployments \
299 are unaffected in practice."
300 );
301 let val = h.get_raw(key.clone()).await?;
302 if val.is_some() {
303 h.remove(key).await?;
304 }
305 Ok(val)
306 }
307 }
308 }
309
310 pub async fn insert_raw(
311 &self,
312 key: impl Into<Vec<u8>>,
313 value: impl Into<Vec<u8>>,
314 ) -> Result<(), AppError> {
315 match self {
316 KeyspaceHandle::Local(h) => h.insert_raw(key, value).await,
317 #[cfg(feature = "vsock-store")]
318 KeyspaceHandle::Vsock(h) => h.insert_raw(key, value).await,
319 }
320 }
321
322 pub async fn get_raw(&self, key: impl Into<Vec<u8>>) -> Result<Option<Vec<u8>>, AppError> {
323 match self {
324 KeyspaceHandle::Local(h) => h.get_raw(key).await,
325 #[cfg(feature = "vsock-store")]
326 KeyspaceHandle::Vsock(h) => h.get_raw(key).await,
327 }
328 }
329
330 pub async fn prefix_iter_raw(
331 &self,
332 prefix: impl Into<Vec<u8>>,
333 ) -> Result<Vec<RawKvPair>, AppError> {
334 match self {
335 KeyspaceHandle::Local(h) => h.prefix_iter_raw(prefix).await,
336 #[cfg(feature = "vsock-store")]
337 KeyspaceHandle::Vsock(h) => h.prefix_iter_raw(prefix).await,
338 }
339 }
340
341 pub async fn range_from_raw(
350 &self,
351 from: impl Into<Vec<u8>>,
352 ) -> Result<Vec<RawKvPair>, AppError> {
353 match self {
354 KeyspaceHandle::Local(h) => h.range_from_raw(from).await,
355 #[cfg(feature = "vsock-store")]
356 KeyspaceHandle::Vsock(h) => h.range_from_raw(from).await,
357 }
358 }
359
360 pub async fn prefix_keys(&self, prefix: impl Into<Vec<u8>>) -> Result<Vec<Vec<u8>>, AppError> {
361 match self {
362 KeyspaceHandle::Local(h) => h.prefix_keys(prefix).await,
363 #[cfg(feature = "vsock-store")]
364 KeyspaceHandle::Vsock(h) => h.prefix_keys(prefix).await,
365 }
366 }
367
368 pub async fn approximate_len(&self) -> Result<usize, AppError> {
369 match self {
370 KeyspaceHandle::Local(h) => h.approximate_len().await,
371 #[cfg(feature = "vsock-store")]
372 KeyspaceHandle::Vsock(h) => h.approximate_len().await,
373 }
374 }
375
376 pub async fn swap<V: Serialize>(
377 &self,
378 old_key: impl Into<Vec<u8>>,
379 new_key: impl Into<Vec<u8>>,
380 value: &V,
381 ) -> Result<bool, AppError> {
382 match self {
383 KeyspaceHandle::Local(h) => h.swap(old_key, new_key, value).await,
384 #[cfg(feature = "vsock-store")]
385 KeyspaceHandle::Vsock(h) => h.swap(old_key, new_key, value).await,
386 }
387 }
388}
389
390type WriteLocks =
406 std::sync::Arc<std::sync::Mutex<HashMap<String, std::sync::Arc<std::sync::Mutex<()>>>>>;
407
408#[derive(Clone)]
409pub struct LocalStore {
410 db: fjall::Database,
411 write_locks: WriteLocks,
412}
413
414#[derive(Clone)]
415pub struct LocalKeyspaceHandle {
416 keyspace: fjall::Keyspace,
417 name: String,
421 db: fjall::Database,
425 write_lock: std::sync::Arc<std::sync::Mutex<()>>,
428 #[cfg(feature = "encryption")]
429 encryption_key: Option<std::sync::Arc<zeroize::Zeroizing<[u8; 32]>>>,
430}
431
432fn lock_writes(lock: &std::sync::Mutex<()>) -> std::sync::MutexGuard<'_, ()> {
437 lock.lock()
438 .unwrap_or_else(std::sync::PoisonError::into_inner)
439}
440
441impl LocalStore {
442 pub fn open(config: &StoreConfig) -> Result<Self, AppError> {
443 std::fs::create_dir_all(&config.data_dir).map_err(AppError::Io)?;
444 info!(path = %config.data_dir.display(), "opening store");
445 let db = fjall::Database::builder(&config.data_dir).open()?;
446 Ok(Self {
447 db,
448 write_locks: WriteLocks::default(),
449 })
450 }
451
452 pub fn keyspace(&self, name: &str) -> Result<LocalKeyspaceHandle, AppError> {
453 let keyspace = self.db.keyspace(name, KeyspaceCreateOptions::default)?;
454 let write_lock = self
455 .write_locks
456 .lock()
457 .unwrap_or_else(std::sync::PoisonError::into_inner)
458 .entry(name.to_string())
459 .or_default()
460 .clone();
461 Ok(LocalKeyspaceHandle {
462 keyspace,
463 name: name.to_string(),
464 db: self.db.clone(),
465 write_lock,
466 #[cfg(feature = "encryption")]
467 encryption_key: None,
468 })
469 }
470
471 pub async fn persist(&self) -> Result<(), AppError> {
472 let db = self.db.clone();
473 tokio::task::spawn_blocking(move || db.persist(PersistMode::SyncAll))
474 .await
475 .map_err(|e| AppError::Internal(format!("blocking task panicked: {e}")))??;
476 Ok(())
477 }
478}
479
480impl LocalKeyspaceHandle {
481 #[cfg(feature = "encryption")]
482 pub fn with_encryption(mut self, key: [u8; 32]) -> Self {
483 self.encryption_key = Some(std::sync::Arc::new(zeroize::Zeroizing::new(key)));
484 self
485 }
486
487 pub fn is_encrypted(&self) -> bool {
488 #[cfg(feature = "encryption")]
489 {
490 self.encryption_key.is_some()
491 }
492 #[cfg(not(feature = "encryption"))]
493 {
494 false
495 }
496 }
497
498 pub async fn persist(&self) -> Result<(), AppError> {
501 let db = self.db.clone();
502 blocking_with_timeout(move || Ok(db.persist(PersistMode::SyncAll)?)).await
503 }
504
505 pub async fn insert<V: Serialize>(
506 &self,
507 key: impl Into<Vec<u8>>,
508 value: &V,
509 ) -> Result<(), AppError> {
510 let key = key.into();
511 let bytes = serde_json::to_vec(value)?;
512 let bytes = self.maybe_encrypt(&key, bytes)?;
513 let ks = self.keyspace.clone();
514 blocking_with_timeout(move || Ok(ks.insert(key, bytes)?)).await
515 }
516
517 pub async fn get<V: DeserializeOwned + Send + 'static>(
518 &self,
519 key: impl Into<Vec<u8>>,
520 ) -> Result<Option<V>, AppError> {
521 let key = key.into();
522 let ks = self.keyspace.clone();
523 #[cfg(feature = "encryption")]
524 let enc_key = self.encryption_key.clone();
525 #[cfg(feature = "encryption")]
526 let name = self.name.clone();
527 blocking_with_timeout(move || match ks.get(&key)? {
528 Some(bytes) => {
529 #[cfg(feature = "encryption")]
530 let bytes = {
531 let k = enc_key.as_ref().map(|arc| &***arc);
532 encryption::maybe_decrypt_bytes(k, &name, &key, &bytes)?
533 };
534 #[cfg(not(feature = "encryption"))]
535 let bytes = bytes.to_vec();
536 Ok(Some(serde_json::from_slice(&bytes)?))
537 }
538 None => Ok(None),
539 })
540 .await
541 }
542
543 pub async fn remove(&self, key: impl Into<Vec<u8>>) -> Result<(), AppError> {
544 let key = key.into();
545 let ks = self.keyspace.clone();
546 blocking_with_timeout(move || Ok(ks.remove(key)?)).await
547 }
548
549 pub async fn take_raw(&self, key: impl Into<Vec<u8>>) -> Result<Option<Vec<u8>>, AppError> {
563 let key = key.into();
564 let ks = self.keyspace.clone();
565 let lock = self.write_lock.clone();
566 #[cfg(feature = "encryption")]
567 let enc_key = self.encryption_key.clone();
568 #[cfg(feature = "encryption")]
569 let name = self.name.clone();
570 blocking_with_timeout(move || {
571 let _guard = lock_writes(&lock);
572 match ks.get(&key)? {
573 Some(bytes) => {
574 ks.remove(&key)?;
575 #[cfg(feature = "encryption")]
576 let bytes = {
577 let k = enc_key.as_ref().map(|arc| &***arc);
578 encryption::maybe_decrypt_bytes(k, &name, &key, &bytes)?
579 };
580 #[cfg(not(feature = "encryption"))]
581 let bytes = bytes.to_vec();
582 Ok(Some(bytes))
583 }
584 None => Ok(None),
585 }
586 })
587 .await
588 }
589
590 pub async fn insert_raw(
591 &self,
592 key: impl Into<Vec<u8>>,
593 value: impl Into<Vec<u8>>,
594 ) -> Result<(), AppError> {
595 let key = key.into();
596 let value = self.maybe_encrypt(&key, value.into())?;
597 let ks = self.keyspace.clone();
598 blocking_with_timeout(move || Ok(ks.insert(key, value)?)).await
599 }
600
601 pub async fn get_raw(&self, key: impl Into<Vec<u8>>) -> Result<Option<Vec<u8>>, AppError> {
602 let key = key.into();
603 let ks = self.keyspace.clone();
604 #[cfg(feature = "encryption")]
605 let enc_key = self.encryption_key.clone();
606 #[cfg(feature = "encryption")]
607 let name = self.name.clone();
608 blocking_with_timeout(move || match ks.get(&key)? {
609 Some(bytes) => {
610 #[cfg(feature = "encryption")]
611 let bytes = {
612 let k = enc_key.as_ref().map(|arc| &***arc);
613 encryption::maybe_decrypt_bytes(k, &name, &key, &bytes)?
614 };
615 #[cfg(not(feature = "encryption"))]
616 let bytes = bytes.to_vec();
617 Ok(Some(bytes))
618 }
619 None => Ok(None),
620 })
621 .await
622 }
623
624 pub async fn prefix_iter_raw(
625 &self,
626 prefix: impl Into<Vec<u8>>,
627 ) -> Result<Vec<RawKvPair>, AppError> {
628 let prefix = prefix.into();
629 let ks = self.keyspace.clone();
630 #[cfg(feature = "encryption")]
631 let enc_key = self.encryption_key.clone();
632 #[cfg(feature = "encryption")]
633 let name = self.name.clone();
634 blocking_with_timeout(move || {
635 let mut results = Vec::new();
636 for guard in ks.prefix(&prefix) {
637 let (key, value) = guard.into_inner()?;
638 #[cfg(feature = "encryption")]
639 let value = {
640 let k = enc_key.as_ref().map(|arc| &***arc);
641 encryption::maybe_decrypt_bytes(k, &name, &key, &value)?
642 };
643 #[cfg(not(feature = "encryption"))]
644 let value = value.to_vec();
645 results.push((key.to_vec(), value));
646 }
647 Ok(results)
648 })
649 .await
650 }
651
652 pub async fn range_from_raw(
655 &self,
656 from: impl Into<Vec<u8>>,
657 ) -> Result<Vec<RawKvPair>, AppError> {
658 let from = from.into();
659 let ks = self.keyspace.clone();
660 #[cfg(feature = "encryption")]
661 let enc_key = self.encryption_key.clone();
662 #[cfg(feature = "encryption")]
663 let name = self.name.clone();
664 blocking_with_timeout(move || {
665 let mut results = Vec::new();
666 for guard in ks.range(from..) {
667 let (key, value) = guard.into_inner()?;
668 #[cfg(feature = "encryption")]
669 let value = {
670 let k = enc_key.as_ref().map(|arc| &***arc);
671 encryption::maybe_decrypt_bytes(k, &name, &key, &value)?
672 };
673 #[cfg(not(feature = "encryption"))]
674 let value = value.to_vec();
675 results.push((key.to_vec(), value));
676 }
677 Ok(results)
678 })
679 .await
680 }
681
682 pub async fn prefix_keys(&self, prefix: impl Into<Vec<u8>>) -> Result<Vec<Vec<u8>>, AppError> {
683 let prefix = prefix.into();
684 let ks = self.keyspace.clone();
685 blocking_with_timeout(move || {
686 let mut results = Vec::new();
687 for guard in ks.prefix(&prefix) {
688 let (key, _value) = guard.into_inner()?;
689 results.push(key.to_vec());
690 }
691 Ok(results)
692 })
693 .await
694 }
695
696 pub async fn approximate_len(&self) -> Result<usize, AppError> {
697 let ks = self.keyspace.clone();
698 blocking_with_timeout(move || Ok(ks.approximate_len())).await
699 }
700
701 pub async fn swap<V: Serialize>(
702 &self,
703 old_key: impl Into<Vec<u8>>,
704 new_key: impl Into<Vec<u8>>,
705 value: &V,
706 ) -> Result<bool, AppError> {
707 let old_key = old_key.into();
708 let new_key = new_key.into();
709 let bytes = serde_json::to_vec(value)?;
710 let bytes = self.maybe_encrypt(&new_key, bytes)?;
712 let ks = self.keyspace.clone();
713 let lock = self.write_lock.clone();
714 blocking_with_timeout(move || {
715 let _guard = lock_writes(&lock);
716 if ks.contains_key(&new_key)? {
717 return Ok(false);
718 }
719 ks.insert(&new_key, bytes)?;
720 ks.remove(&old_key)?;
721 Ok(true)
722 })
723 .await
724 }
725
726 pub async fn insert_if_absent<V: Serialize>(
730 &self,
731 key: impl Into<Vec<u8>>,
732 value: &V,
733 ) -> Result<bool, AppError> {
734 let key = key.into();
735 let bytes = serde_json::to_vec(value)?;
736 self.insert_bytes_if_absent(key, bytes).await
737 }
738
739 pub async fn insert_raw_if_absent(
742 &self,
743 key: impl Into<Vec<u8>>,
744 value: impl Into<Vec<u8>>,
745 ) -> Result<bool, AppError> {
746 self.insert_bytes_if_absent(key.into(), value.into()).await
747 }
748
749 async fn insert_bytes_if_absent(&self, key: Vec<u8>, bytes: Vec<u8>) -> Result<bool, AppError> {
753 let bytes = self.maybe_encrypt(&key, bytes)?;
754 let ks = self.keyspace.clone();
755 let lock = self.write_lock.clone();
756 blocking_with_timeout(move || {
757 let _guard = lock_writes(&lock);
758 if ks.contains_key(&key)? {
759 return Ok(false);
760 }
761 ks.insert(&key, bytes)?;
762 Ok(true)
763 })
764 .await
765 }
766
767 fn maybe_encrypt(&self, store_key: &[u8], plaintext: Vec<u8>) -> Result<Vec<u8>, AppError> {
768 #[cfg(feature = "encryption")]
769 {
770 match self.encryption_key.as_ref().map(|arc| &***arc) {
771 Some(key) => encryption::encrypt_value(key, &self.name, store_key, &plaintext),
772 None => Ok(plaintext),
773 }
774 }
775 #[cfg(not(feature = "encryption"))]
776 {
777 let _ = store_key;
778 Ok(plaintext)
779 }
780 }
781}
782
783#[cfg(test)]
784mod tests {
785 use super::*;
786
787 fn temp_store() -> (Store, tempfile::TempDir) {
788 let dir = tempfile::tempdir().expect("failed to create temp dir");
789 let config = StoreConfig {
790 data_dir: dir.path().to_path_buf(),
791 };
792 let store = Store::open(&config).expect("failed to open store");
793 (store, dir)
794 }
795
796 #[tokio::test]
797 async fn persist_survives_store_reopen() {
798 let dir = tempfile::tempdir().expect("tempdir");
803 let path = dir.path().to_path_buf();
804 {
805 let store = Store::open(&StoreConfig {
806 data_dir: path.clone(),
807 })
808 .expect("open store");
809 let ks = store.keyspace("keys").unwrap();
810 ks.insert_raw("carveout:closed", b"admin-did".to_vec())
811 .await
812 .unwrap();
813 ks.persist().await.unwrap();
814 }
816 let store = Store::open(&StoreConfig { data_dir: path }).expect("reopen store");
817 let ks = store.keyspace("keys").unwrap();
818 assert_eq!(
819 ks.get_raw("carveout:closed").await.unwrap().as_deref(),
820 Some(b"admin-did".as_slice()),
821 "a persisted write must survive a store reopen"
822 );
823 }
824
825 #[tokio::test]
826 async fn insert_if_absent_claims_only_once() {
827 let (store, _dir) = temp_store();
828 let ks = store.keyspace("test").unwrap();
829
830 assert!(
831 ks.insert_if_absent("k", &"first".to_string())
832 .await
833 .unwrap(),
834 "first claim must succeed"
835 );
836 assert!(
837 !ks.insert_if_absent("k", &"second".to_string())
838 .await
839 .unwrap(),
840 "second claim must be refused"
841 );
842 let got: String = ks.get("k").await.unwrap().unwrap();
843 assert_eq!(got, "first", "loser must not overwrite the stored value");
844 }
845
846 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
847 async fn insert_if_absent_under_concurrency_admits_exactly_one() {
848 let (store, _dir) = temp_store();
849 let ks = store.keyspace("test").unwrap();
850
851 let mut handles = Vec::new();
852 for i in 0..16u32 {
853 let ks = ks.clone();
854 handles.push(tokio::spawn(async move {
855 ks.insert_if_absent("contested", &format!("writer-{i}"))
856 .await
857 .unwrap()
858 }));
859 }
860 let mut winners = 0;
861 for h in handles {
862 if h.await.unwrap() {
863 winners += 1;
864 }
865 }
866 assert_eq!(winners, 1, "exactly one racing claim may win");
867 }
868
869 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
870 async fn take_raw_under_concurrency_admits_exactly_one() {
871 let (store, _dir) = temp_store();
876 store
877 .keyspace("test")
878 .unwrap()
879 .insert("token", &"refresh".to_string())
880 .await
881 .unwrap();
882
883 let mut handles = Vec::new();
884 for _ in 0..16 {
885 let ks = store.keyspace("test").unwrap();
886 handles.push(tokio::spawn(
887 async move { ks.take_raw("token").await.unwrap() },
888 ));
889 }
890 let mut claimed = 0;
891 for h in handles {
892 if h.await.unwrap().is_some() {
893 claimed += 1;
894 }
895 }
896 assert_eq!(claimed, 1, "exactly one concurrent take_raw may claim");
897 }
898
899 #[tokio::test]
900 async fn test_basic_roundtrip() {
901 let (store, _dir) = temp_store();
902 let ks = store.keyspace("test").unwrap();
903
904 #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
905 struct TestRecord {
906 id: String,
907 value: u64,
908 }
909
910 let record = TestRecord {
911 id: "test-1".into(),
912 value: 42,
913 };
914
915 ks.insert("key:test-1", &record).await.unwrap();
916 let got: TestRecord = ks.get("key:test-1").await.unwrap().unwrap();
917 assert_eq!(got, record);
918 }
919
920 #[tokio::test]
921 async fn test_prefix_iter() {
922 let (store, _dir) = temp_store();
923 let ks = store.keyspace("test").unwrap();
924
925 for i in 0..5 {
926 ks.insert_raw(format!("prefix:{i}"), format!("value-{i}").into_bytes())
927 .await
928 .unwrap();
929 }
930
931 let raw = ks.prefix_iter_raw("prefix:").await.unwrap();
932 assert_eq!(raw.len(), 5);
933 }
934
935 #[tokio::test]
936 async fn test_range_from_raw_seeks_to_lower_bound() {
937 let (store, _dir) = temp_store();
938 let ks = store.keyspace("test").unwrap();
939
940 for k in ["2026-01:a", "2026-02:b", "2026-03:c", "2026-04:d"] {
943 ks.insert_raw(k.as_bytes().to_vec(), b"v".to_vec())
944 .await
945 .unwrap();
946 }
947
948 let rows = ks.range_from_raw(b"2026-03:".to_vec()).await.unwrap();
950 let keys: Vec<String> = rows
951 .iter()
952 .map(|(k, _)| String::from_utf8(k.clone()).unwrap())
953 .collect();
954 assert_eq!(keys, vec!["2026-03:c", "2026-04:d"]);
955
956 assert_eq!(ks.range_from_raw(Vec::new()).await.unwrap().len(), 4);
958 assert!(
960 ks.range_from_raw(b"2026-99:".to_vec())
961 .await
962 .unwrap()
963 .is_empty()
964 );
965 }
966
967 #[tokio::test]
968 async fn test_remove() {
969 let (store, _dir) = temp_store();
970 let ks = store.keyspace("test").unwrap();
971
972 ks.insert_raw("key", b"value".to_vec()).await.unwrap();
973 assert!(ks.get_raw("key").await.unwrap().is_some());
974
975 ks.remove("key").await.unwrap();
976 assert!(ks.get_raw("key").await.unwrap().is_none());
977 }
978
979 #[tokio::test]
980 async fn test_swap() {
981 let (store, _dir) = temp_store();
982 let ks = store.keyspace("test").unwrap();
983
984 ks.insert("old", &"value").await.unwrap();
985 let swapped = ks.swap("old", "new", &"value").await.unwrap();
986 assert!(swapped);
987 assert!(ks.get::<String>("old").await.unwrap().is_none());
988 assert!(ks.get::<String>("new").await.unwrap().is_some());
989 }
990
991 #[tokio::test]
992 async fn test_passthrough_mode_no_encryption() {
993 let (store, _dir) = temp_store();
994 let ks = store.keyspace("plain").unwrap();
995 assert!(!ks.is_encrypted());
996
997 ks.insert_raw("test", b"visible".to_vec()).await.unwrap();
998 let raw = ks.get_raw("test").await.unwrap().unwrap();
999 assert_eq!(raw, b"visible");
1000 }
1001
1002 #[cfg(feature = "encryption")]
1003 #[tokio::test]
1004 async fn test_encrypted_roundtrip() {
1005 let (store, _dir) = temp_store();
1006 let ks = store
1007 .keyspace("encrypted")
1008 .unwrap()
1009 .with_encryption([0xAB; 32]);
1010
1011 assert!(ks.is_encrypted());
1012
1013 ks.insert_raw("raw:test", b"hello world".to_vec())
1015 .await
1016 .unwrap();
1017 let raw = ks.get_raw("raw:test").await.unwrap().unwrap();
1018 assert_eq!(raw, b"hello world");
1019
1020 ks.insert("json:test", &"encrypted value").await.unwrap();
1022 let got: String = ks.get("json:test").await.unwrap().unwrap();
1023 assert_eq!(got, "encrypted value");
1024 }
1025
1026 #[cfg(feature = "encryption")]
1032 #[tokio::test]
1033 async fn encrypted_value_cannot_be_pasted_to_another_key() {
1034 let (store, _dir) = temp_store();
1035 let key = [0x55; 32];
1036 let ks = store.keyspace("acl").unwrap().with_encryption(key);
1037
1038 ks.insert_raw("acl:victim", b"admin-row".to_vec())
1039 .await
1040 .unwrap();
1041
1042 let raw = store.keyspace("acl").unwrap();
1046 let stolen = raw.get_raw("acl:victim").await.unwrap().unwrap();
1047 raw.insert_raw("acl:attacker", stolen).await.unwrap();
1048
1049 let err = ks.get_raw("acl:attacker").await;
1052 assert!(
1053 err.is_err(),
1054 "a ciphertext pasted to a different key must fail AAD authentication"
1055 );
1056 assert_eq!(
1058 ks.get_raw("acl:victim").await.unwrap().unwrap(),
1059 b"admin-row"
1060 );
1061 }
1062
1063 #[cfg(feature = "encryption")]
1064 #[tokio::test]
1065 async fn test_encrypted_data_is_actually_encrypted_on_disk() {
1066 let (store, _dir) = temp_store();
1067 let enc_key = [0x42; 32];
1068
1069 let ks_enc = store.keyspace("secrets").unwrap().with_encryption(enc_key);
1071 ks_enc
1072 .insert_raw("test", b"plaintext secret".to_vec())
1073 .await
1074 .unwrap();
1075
1076 let ks_raw = store.keyspace("secrets").unwrap();
1078 let on_disk = ks_raw.get_raw("test").await.unwrap().unwrap();
1079
1080 assert_ne!(on_disk, b"plaintext secret");
1082 assert!(on_disk.len() >= 12 + 16 + 16);
1084
1085 let decrypted = ks_enc.get_raw("test").await.unwrap().unwrap();
1087 assert_eq!(decrypted, b"plaintext secret");
1088 }
1089
1090 #[cfg(feature = "encryption")]
1094 #[tokio::test]
1095 async fn migrate_to_encrypted_converts_legacy_plaintext() {
1096 let (store, _dir) = temp_store();
1097 let key = [0x33; 32];
1098
1099 let bare = store.keyspace("install").unwrap();
1101 bare.insert_raw("token:a", b"ephemeral-key-bytes".to_vec())
1102 .await
1103 .unwrap();
1104 bare.insert("token:b", &"json-state".to_string())
1105 .await
1106 .unwrap();
1107
1108 let migrated = bare.migrate_to_encrypted(key).await.unwrap();
1110 assert_eq!(migrated, 2, "both legacy rows must be encrypted");
1111
1112 let on_disk = bare.get_raw("token:a").await.unwrap().unwrap();
1115 assert_ne!(on_disk, b"ephemeral-key-bytes");
1116 assert!(
1117 on_disk.starts_with(b"VAE1"),
1118 "migrated row must carry the v1 encryption magic"
1119 );
1120
1121 let enc = store.keyspace("install").unwrap().with_encryption(key);
1123 assert_eq!(
1124 enc.get_raw("token:a").await.unwrap().unwrap(),
1125 b"ephemeral-key-bytes"
1126 );
1127 let b: String = enc.get("token:b").await.unwrap().unwrap();
1128 assert_eq!(b, "json-state");
1129 }
1130
1131 #[cfg(feature = "encryption")]
1135 #[tokio::test]
1136 async fn migrate_to_encrypted_is_idempotent_and_crash_safe() {
1137 let (store, _dir) = temp_store();
1138 let key = [0x44; 32];
1139
1140 let bare = store.keyspace("passkey").unwrap();
1141 bare.insert_raw("row:1", b"plaintext-one".to_vec())
1142 .await
1143 .unwrap();
1144
1145 assert_eq!(bare.migrate_to_encrypted(key).await.unwrap(), 1);
1147
1148 bare.insert_raw("row:2", b"plaintext-two".to_vec())
1151 .await
1152 .unwrap();
1153
1154 assert_eq!(bare.migrate_to_encrypted(key).await.unwrap(), 1);
1157
1158 assert_eq!(bare.migrate_to_encrypted(key).await.unwrap(), 0);
1160
1161 let enc = store.keyspace("passkey").unwrap().with_encryption(key);
1162 assert_eq!(
1163 enc.get_raw("row:1").await.unwrap().unwrap(),
1164 b"plaintext-one"
1165 );
1166 assert_eq!(
1167 enc.get_raw("row:2").await.unwrap().unwrap(),
1168 b"plaintext-two"
1169 );
1170 }
1171
1172 #[cfg(feature = "encryption")]
1176 #[tokio::test]
1177 async fn migrate_to_encrypted_rejects_encrypted_handle() {
1178 let (store, _dir) = temp_store();
1179 let enc = store
1180 .keyspace("install")
1181 .unwrap()
1182 .with_encryption([0x55; 32]);
1183 assert!(enc.migrate_to_encrypted([0x55; 32]).await.is_err());
1184 }
1185}