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
42const FJALL_VERSION_MARKER: &str = "version";
45
46pub fn local_store_exists(data_dir: &std::path::Path) -> bool {
57 data_dir.join(FJALL_VERSION_MARKER).is_file()
58}
59
60#[derive(Clone)]
69pub enum Store {
70 Local(LocalStore),
72 #[cfg(feature = "vsock-store")]
74 Vsock(vsock::VsockStore),
75}
76
77impl Store {
78 pub fn open(config: &StoreConfig) -> Result<Self, AppError> {
80 Ok(Store::Local(LocalStore::open(config)?))
81 }
82
83 #[cfg(feature = "vsock-store")]
85 pub async fn connect_vsock(port: Option<u32>) -> Result<Self, AppError> {
86 Ok(Store::Vsock(vsock::VsockStore::connect(port).await?))
87 }
88
89 pub fn keyspace(&self, name: &str) -> Result<KeyspaceHandle, AppError> {
90 match self {
91 Store::Local(s) => Ok(KeyspaceHandle::Local(s.keyspace(name)?)),
92 #[cfg(feature = "vsock-store")]
93 Store::Vsock(s) => Ok(KeyspaceHandle::Vsock(s.keyspace(name)?)),
94 }
95 }
96
97 pub async fn persist(&self) -> Result<(), AppError> {
98 match self {
99 Store::Local(s) => s.persist().await,
100 #[cfg(feature = "vsock-store")]
101 Store::Vsock(s) => s.persist().await,
102 }
103 }
104}
105
106#[derive(Clone)]
115pub enum KeyspaceHandle {
116 Local(LocalKeyspaceHandle),
117 #[cfg(feature = "vsock-store")]
118 Vsock(vsock::VsockKeyspaceHandle),
119}
120
121impl KeyspaceHandle {
122 #[cfg(feature = "encryption")]
123 pub fn with_encryption(self, key: [u8; 32]) -> Self {
124 match self {
125 KeyspaceHandle::Local(h) => KeyspaceHandle::Local(h.with_encryption(key)),
126 #[cfg(feature = "vsock-store")]
127 KeyspaceHandle::Vsock(h) => KeyspaceHandle::Vsock(h.with_encryption(key)),
128 }
129 }
130
131 pub fn is_encrypted(&self) -> bool {
132 match self {
133 KeyspaceHandle::Local(h) => h.is_encrypted(),
134 #[cfg(feature = "vsock-store")]
135 KeyspaceHandle::Vsock(h) => h.is_encrypted(),
136 }
137 }
138
139 #[cfg(feature = "encryption")]
160 pub async fn migrate_to_encrypted(&self, key: [u8; 32]) -> Result<usize, AppError> {
161 if self.is_encrypted() {
162 return Err(AppError::Internal(
163 "migrate_to_encrypted must be called on a bare (unencrypted) keyspace handle"
164 .into(),
165 ));
166 }
167 let rows = self.prefix_iter_raw(Vec::<u8>::new()).await?;
171 let encrypted = self.clone().with_encryption(key);
172 let mut migrated = 0usize;
173 for (k, v) in rows {
174 if encryption::is_v1_encrypted(&v) {
175 continue;
176 }
177 encrypted.insert_raw(k, v).await?;
180 migrated += 1;
181 }
182 Ok(migrated)
183 }
184
185 pub async fn persist(&self) -> Result<(), AppError> {
194 match self {
195 KeyspaceHandle::Local(h) => h.persist().await,
196 #[cfg(feature = "vsock-store")]
197 KeyspaceHandle::Vsock(h) => h.persist().await,
198 }
199 }
200
201 pub async fn insert<V: Serialize>(
202 &self,
203 key: impl Into<Vec<u8>>,
204 value: &V,
205 ) -> Result<(), AppError> {
206 match self {
207 KeyspaceHandle::Local(h) => h.insert(key, value).await,
208 #[cfg(feature = "vsock-store")]
209 KeyspaceHandle::Vsock(h) => h.insert(key, value).await,
210 }
211 }
212
213 pub async fn insert_if_absent<V: Serialize>(
227 &self,
228 key: impl Into<Vec<u8>>,
229 value: &V,
230 ) -> Result<bool, AppError> {
231 match self {
232 KeyspaceHandle::Local(h) => h.insert_if_absent(key, value).await,
233 #[cfg(feature = "vsock-store")]
234 KeyspaceHandle::Vsock(h) => {
235 tracing::warn!(
236 "KeyspaceHandle::Vsock::insert_if_absent using non-atomic get+insert \
237 fallback; vsock proto lacks a native insert-if-absent opcode. \
238 Single-replica TEE deployments are unaffected in practice."
239 );
240 let key = key.into();
241 if h.get_raw(key.clone()).await?.is_some() {
242 return Ok(false);
243 }
244 h.insert(key, value).await?;
245 Ok(true)
246 }
247 }
248 }
249
250 pub async fn insert_raw_if_absent(
254 &self,
255 key: impl Into<Vec<u8>>,
256 value: impl Into<Vec<u8>>,
257 ) -> Result<bool, AppError> {
258 match self {
259 KeyspaceHandle::Local(h) => h.insert_raw_if_absent(key, value).await,
260 #[cfg(feature = "vsock-store")]
261 KeyspaceHandle::Vsock(h) => {
262 tracing::warn!(
263 "KeyspaceHandle::Vsock::insert_raw_if_absent using non-atomic get+insert \
264 fallback; vsock proto lacks a native insert-if-absent opcode. \
265 Single-replica TEE deployments are unaffected in practice."
266 );
267 let key = key.into();
268 if h.get_raw(key.clone()).await?.is_some() {
269 return Ok(false);
270 }
271 h.insert_raw(key, value).await?;
272 Ok(true)
273 }
274 }
275 }
276
277 pub async fn get<V: DeserializeOwned + Send + 'static>(
278 &self,
279 key: impl Into<Vec<u8>>,
280 ) -> Result<Option<V>, AppError> {
281 match self {
282 KeyspaceHandle::Local(h) => h.get(key).await,
283 #[cfg(feature = "vsock-store")]
284 KeyspaceHandle::Vsock(h) => h.get(key).await,
285 }
286 }
287
288 pub async fn remove(&self, key: impl Into<Vec<u8>>) -> Result<(), AppError> {
289 match self {
290 KeyspaceHandle::Local(h) => h.remove(key).await,
291 #[cfg(feature = "vsock-store")]
292 KeyspaceHandle::Vsock(h) => h.remove(key).await,
293 }
294 }
295
296 pub async fn take_raw(&self, key: impl Into<Vec<u8>>) -> Result<Option<Vec<u8>>, AppError> {
309 let key = key.into();
310 match self {
311 KeyspaceHandle::Local(h) => h.take_raw(key).await,
312 #[cfg(feature = "vsock-store")]
313 KeyspaceHandle::Vsock(h) => {
314 tracing::warn!(
315 "KeyspaceHandle::Vsock::take_raw using non-atomic get+remove fallback; \
316 vsock proto lacks a native take opcode. Single-replica TEE deployments \
317 are unaffected in practice."
318 );
319 let val = h.get_raw(key.clone()).await?;
320 if val.is_some() {
321 h.remove(key).await?;
322 }
323 Ok(val)
324 }
325 }
326 }
327
328 pub async fn insert_raw(
329 &self,
330 key: impl Into<Vec<u8>>,
331 value: impl Into<Vec<u8>>,
332 ) -> Result<(), AppError> {
333 match self {
334 KeyspaceHandle::Local(h) => h.insert_raw(key, value).await,
335 #[cfg(feature = "vsock-store")]
336 KeyspaceHandle::Vsock(h) => h.insert_raw(key, value).await,
337 }
338 }
339
340 pub async fn get_raw(&self, key: impl Into<Vec<u8>>) -> Result<Option<Vec<u8>>, AppError> {
341 match self {
342 KeyspaceHandle::Local(h) => h.get_raw(key).await,
343 #[cfg(feature = "vsock-store")]
344 KeyspaceHandle::Vsock(h) => h.get_raw(key).await,
345 }
346 }
347
348 pub async fn prefix_iter_raw(
349 &self,
350 prefix: impl Into<Vec<u8>>,
351 ) -> Result<Vec<RawKvPair>, AppError> {
352 match self {
353 KeyspaceHandle::Local(h) => h.prefix_iter_raw(prefix).await,
354 #[cfg(feature = "vsock-store")]
355 KeyspaceHandle::Vsock(h) => h.prefix_iter_raw(prefix).await,
356 }
357 }
358
359 pub async fn range_from_raw(
368 &self,
369 from: impl Into<Vec<u8>>,
370 ) -> Result<Vec<RawKvPair>, AppError> {
371 match self {
372 KeyspaceHandle::Local(h) => h.range_from_raw(from).await,
373 #[cfg(feature = "vsock-store")]
374 KeyspaceHandle::Vsock(h) => h.range_from_raw(from).await,
375 }
376 }
377
378 pub async fn prefix_keys(&self, prefix: impl Into<Vec<u8>>) -> Result<Vec<Vec<u8>>, AppError> {
379 match self {
380 KeyspaceHandle::Local(h) => h.prefix_keys(prefix).await,
381 #[cfg(feature = "vsock-store")]
382 KeyspaceHandle::Vsock(h) => h.prefix_keys(prefix).await,
383 }
384 }
385
386 pub async fn approximate_len(&self) -> Result<usize, AppError> {
387 match self {
388 KeyspaceHandle::Local(h) => h.approximate_len().await,
389 #[cfg(feature = "vsock-store")]
390 KeyspaceHandle::Vsock(h) => h.approximate_len().await,
391 }
392 }
393
394 pub async fn swap<V: Serialize>(
395 &self,
396 old_key: impl Into<Vec<u8>>,
397 new_key: impl Into<Vec<u8>>,
398 value: &V,
399 ) -> Result<bool, AppError> {
400 match self {
401 KeyspaceHandle::Local(h) => h.swap(old_key, new_key, value).await,
402 #[cfg(feature = "vsock-store")]
403 KeyspaceHandle::Vsock(h) => h.swap(old_key, new_key, value).await,
404 }
405 }
406}
407
408type WriteLocks =
424 std::sync::Arc<std::sync::Mutex<HashMap<String, std::sync::Arc<std::sync::Mutex<()>>>>>;
425
426#[derive(Clone)]
427pub struct LocalStore {
428 db: fjall::Database,
429 write_locks: WriteLocks,
430}
431
432#[derive(Clone)]
433pub struct LocalKeyspaceHandle {
434 keyspace: fjall::Keyspace,
435 name: String,
439 db: fjall::Database,
443 write_lock: std::sync::Arc<std::sync::Mutex<()>>,
446 #[cfg(feature = "encryption")]
447 encryption_key: Option<std::sync::Arc<zeroize::Zeroizing<[u8; 32]>>>,
448}
449
450fn lock_writes(lock: &std::sync::Mutex<()>) -> std::sync::MutexGuard<'_, ()> {
455 lock.lock()
456 .unwrap_or_else(std::sync::PoisonError::into_inner)
457}
458
459impl LocalStore {
460 pub fn open(config: &StoreConfig) -> Result<Self, AppError> {
461 std::fs::create_dir_all(&config.data_dir).map_err(AppError::Io)?;
462 info!(path = %config.data_dir.display(), "opening store");
463 let db = fjall::Database::builder(&config.data_dir).open()?;
464 Ok(Self {
465 db,
466 write_locks: WriteLocks::default(),
467 })
468 }
469
470 pub fn keyspace(&self, name: &str) -> Result<LocalKeyspaceHandle, AppError> {
471 let keyspace = self.db.keyspace(name, KeyspaceCreateOptions::default)?;
472 let write_lock = self
473 .write_locks
474 .lock()
475 .unwrap_or_else(std::sync::PoisonError::into_inner)
476 .entry(name.to_string())
477 .or_default()
478 .clone();
479 Ok(LocalKeyspaceHandle {
480 keyspace,
481 name: name.to_string(),
482 db: self.db.clone(),
483 write_lock,
484 #[cfg(feature = "encryption")]
485 encryption_key: None,
486 })
487 }
488
489 pub async fn persist(&self) -> Result<(), AppError> {
490 let db = self.db.clone();
491 tokio::task::spawn_blocking(move || db.persist(PersistMode::SyncAll))
492 .await
493 .map_err(|e| AppError::Internal(format!("blocking task panicked: {e}")))??;
494 Ok(())
495 }
496}
497
498impl LocalKeyspaceHandle {
499 #[cfg(feature = "encryption")]
500 pub fn with_encryption(mut self, key: [u8; 32]) -> Self {
501 self.encryption_key = Some(std::sync::Arc::new(zeroize::Zeroizing::new(key)));
502 self
503 }
504
505 pub fn is_encrypted(&self) -> bool {
506 #[cfg(feature = "encryption")]
507 {
508 self.encryption_key.is_some()
509 }
510 #[cfg(not(feature = "encryption"))]
511 {
512 false
513 }
514 }
515
516 pub async fn persist(&self) -> Result<(), AppError> {
519 let db = self.db.clone();
520 blocking_with_timeout(move || Ok(db.persist(PersistMode::SyncAll)?)).await
521 }
522
523 pub async fn insert<V: Serialize>(
524 &self,
525 key: impl Into<Vec<u8>>,
526 value: &V,
527 ) -> Result<(), AppError> {
528 let key = key.into();
529 let bytes = serde_json::to_vec(value)?;
530 let bytes = self.maybe_encrypt(&key, bytes)?;
531 let ks = self.keyspace.clone();
532 blocking_with_timeout(move || Ok(ks.insert(key, bytes)?)).await
533 }
534
535 pub async fn get<V: DeserializeOwned + Send + 'static>(
536 &self,
537 key: impl Into<Vec<u8>>,
538 ) -> Result<Option<V>, AppError> {
539 let key = key.into();
540 let ks = self.keyspace.clone();
541 #[cfg(feature = "encryption")]
542 let enc_key = self.encryption_key.clone();
543 #[cfg(feature = "encryption")]
544 let name = self.name.clone();
545 blocking_with_timeout(move || match ks.get(&key)? {
546 Some(bytes) => {
547 #[cfg(feature = "encryption")]
548 let bytes = {
549 let k = enc_key.as_ref().map(|arc| &***arc);
550 encryption::maybe_decrypt_bytes(k, &name, &key, &bytes)?
551 };
552 #[cfg(not(feature = "encryption"))]
553 let bytes = bytes.to_vec();
554 Ok(Some(serde_json::from_slice(&bytes)?))
555 }
556 None => Ok(None),
557 })
558 .await
559 }
560
561 pub async fn remove(&self, key: impl Into<Vec<u8>>) -> Result<(), AppError> {
562 let key = key.into();
563 let ks = self.keyspace.clone();
564 blocking_with_timeout(move || Ok(ks.remove(key)?)).await
565 }
566
567 pub async fn take_raw(&self, key: impl Into<Vec<u8>>) -> Result<Option<Vec<u8>>, AppError> {
581 let key = key.into();
582 let ks = self.keyspace.clone();
583 let lock = self.write_lock.clone();
584 #[cfg(feature = "encryption")]
585 let enc_key = self.encryption_key.clone();
586 #[cfg(feature = "encryption")]
587 let name = self.name.clone();
588 blocking_with_timeout(move || {
589 let _guard = lock_writes(&lock);
590 match ks.get(&key)? {
591 Some(bytes) => {
592 ks.remove(&key)?;
593 #[cfg(feature = "encryption")]
594 let bytes = {
595 let k = enc_key.as_ref().map(|arc| &***arc);
596 encryption::maybe_decrypt_bytes(k, &name, &key, &bytes)?
597 };
598 #[cfg(not(feature = "encryption"))]
599 let bytes = bytes.to_vec();
600 Ok(Some(bytes))
601 }
602 None => Ok(None),
603 }
604 })
605 .await
606 }
607
608 pub async fn insert_raw(
609 &self,
610 key: impl Into<Vec<u8>>,
611 value: impl Into<Vec<u8>>,
612 ) -> Result<(), AppError> {
613 let key = key.into();
614 let value = self.maybe_encrypt(&key, value.into())?;
615 let ks = self.keyspace.clone();
616 blocking_with_timeout(move || Ok(ks.insert(key, value)?)).await
617 }
618
619 pub async fn get_raw(&self, key: impl Into<Vec<u8>>) -> Result<Option<Vec<u8>>, AppError> {
620 let key = key.into();
621 let ks = self.keyspace.clone();
622 #[cfg(feature = "encryption")]
623 let enc_key = self.encryption_key.clone();
624 #[cfg(feature = "encryption")]
625 let name = self.name.clone();
626 blocking_with_timeout(move || match ks.get(&key)? {
627 Some(bytes) => {
628 #[cfg(feature = "encryption")]
629 let bytes = {
630 let k = enc_key.as_ref().map(|arc| &***arc);
631 encryption::maybe_decrypt_bytes(k, &name, &key, &bytes)?
632 };
633 #[cfg(not(feature = "encryption"))]
634 let bytes = bytes.to_vec();
635 Ok(Some(bytes))
636 }
637 None => Ok(None),
638 })
639 .await
640 }
641
642 pub async fn prefix_iter_raw(
643 &self,
644 prefix: impl Into<Vec<u8>>,
645 ) -> Result<Vec<RawKvPair>, AppError> {
646 let prefix = prefix.into();
647 let ks = self.keyspace.clone();
648 #[cfg(feature = "encryption")]
649 let enc_key = self.encryption_key.clone();
650 #[cfg(feature = "encryption")]
651 let name = self.name.clone();
652 blocking_with_timeout(move || {
653 let mut results = Vec::new();
654 for guard in ks.prefix(&prefix) {
655 let (key, value) = guard.into_inner()?;
656 #[cfg(feature = "encryption")]
657 let value = {
658 let k = enc_key.as_ref().map(|arc| &***arc);
659 encryption::maybe_decrypt_bytes(k, &name, &key, &value)?
660 };
661 #[cfg(not(feature = "encryption"))]
662 let value = value.to_vec();
663 results.push((key.to_vec(), value));
664 }
665 Ok(results)
666 })
667 .await
668 }
669
670 pub async fn range_from_raw(
673 &self,
674 from: impl Into<Vec<u8>>,
675 ) -> Result<Vec<RawKvPair>, AppError> {
676 let from = from.into();
677 let ks = self.keyspace.clone();
678 #[cfg(feature = "encryption")]
679 let enc_key = self.encryption_key.clone();
680 #[cfg(feature = "encryption")]
681 let name = self.name.clone();
682 blocking_with_timeout(move || {
683 let mut results = Vec::new();
684 for guard in ks.range(from..) {
685 let (key, value) = guard.into_inner()?;
686 #[cfg(feature = "encryption")]
687 let value = {
688 let k = enc_key.as_ref().map(|arc| &***arc);
689 encryption::maybe_decrypt_bytes(k, &name, &key, &value)?
690 };
691 #[cfg(not(feature = "encryption"))]
692 let value = value.to_vec();
693 results.push((key.to_vec(), value));
694 }
695 Ok(results)
696 })
697 .await
698 }
699
700 pub async fn prefix_keys(&self, prefix: impl Into<Vec<u8>>) -> Result<Vec<Vec<u8>>, AppError> {
701 let prefix = prefix.into();
702 let ks = self.keyspace.clone();
703 blocking_with_timeout(move || {
704 let mut results = Vec::new();
705 for guard in ks.prefix(&prefix) {
706 let (key, _value) = guard.into_inner()?;
707 results.push(key.to_vec());
708 }
709 Ok(results)
710 })
711 .await
712 }
713
714 pub async fn approximate_len(&self) -> Result<usize, AppError> {
715 let ks = self.keyspace.clone();
716 blocking_with_timeout(move || Ok(ks.approximate_len())).await
717 }
718
719 pub async fn swap<V: Serialize>(
720 &self,
721 old_key: impl Into<Vec<u8>>,
722 new_key: impl Into<Vec<u8>>,
723 value: &V,
724 ) -> Result<bool, AppError> {
725 let old_key = old_key.into();
726 let new_key = new_key.into();
727 let bytes = serde_json::to_vec(value)?;
728 let bytes = self.maybe_encrypt(&new_key, bytes)?;
730 let ks = self.keyspace.clone();
731 let lock = self.write_lock.clone();
732 blocking_with_timeout(move || {
733 let _guard = lock_writes(&lock);
734 if ks.contains_key(&new_key)? {
735 return Ok(false);
736 }
737 ks.insert(&new_key, bytes)?;
738 ks.remove(&old_key)?;
739 Ok(true)
740 })
741 .await
742 }
743
744 pub async fn insert_if_absent<V: Serialize>(
748 &self,
749 key: impl Into<Vec<u8>>,
750 value: &V,
751 ) -> Result<bool, AppError> {
752 let key = key.into();
753 let bytes = serde_json::to_vec(value)?;
754 self.insert_bytes_if_absent(key, bytes).await
755 }
756
757 pub async fn insert_raw_if_absent(
760 &self,
761 key: impl Into<Vec<u8>>,
762 value: impl Into<Vec<u8>>,
763 ) -> Result<bool, AppError> {
764 self.insert_bytes_if_absent(key.into(), value.into()).await
765 }
766
767 async fn insert_bytes_if_absent(&self, key: Vec<u8>, bytes: Vec<u8>) -> Result<bool, AppError> {
771 let bytes = self.maybe_encrypt(&key, bytes)?;
772 let ks = self.keyspace.clone();
773 let lock = self.write_lock.clone();
774 blocking_with_timeout(move || {
775 let _guard = lock_writes(&lock);
776 if ks.contains_key(&key)? {
777 return Ok(false);
778 }
779 ks.insert(&key, bytes)?;
780 Ok(true)
781 })
782 .await
783 }
784
785 fn maybe_encrypt(&self, store_key: &[u8], plaintext: Vec<u8>) -> Result<Vec<u8>, AppError> {
786 #[cfg(feature = "encryption")]
787 {
788 match self.encryption_key.as_ref().map(|arc| &***arc) {
789 Some(key) => encryption::encrypt_value(key, &self.name, store_key, &plaintext),
790 None => Ok(plaintext),
791 }
792 }
793 #[cfg(not(feature = "encryption"))]
794 {
795 let _ = store_key;
796 Ok(plaintext)
797 }
798 }
799}
800
801#[cfg(test)]
802mod tests {
803 use super::*;
804
805 fn temp_store() -> (Store, tempfile::TempDir) {
806 let dir = tempfile::tempdir().expect("failed to create temp dir");
807 let config = StoreConfig {
808 data_dir: dir.path().to_path_buf(),
809 };
810 let store = Store::open(&config).expect("failed to open store");
811 (store, dir)
812 }
813
814 #[test]
815 fn local_store_exists_ignores_a_bare_directory() {
816 let dir = tempfile::tempdir().expect("tempdir");
821 assert!(
822 !local_store_exists(dir.path()),
823 "an empty directory holds no store"
824 );
825
826 std::fs::write(dir.path().join("did.jsonl"), b"{}").expect("write stray file");
827 assert!(
828 !local_store_exists(dir.path()),
829 "a non-empty directory without fjall's marker still holds no store"
830 );
831
832 assert!(
833 !local_store_exists(&dir.path().join("does-not-exist")),
834 "an absent directory holds no store"
835 );
836 }
837
838 #[test]
839 fn local_store_exists_sees_an_opened_store() {
840 let (_store, dir) = temp_store();
841 assert!(
842 local_store_exists(dir.path()),
843 "opening a store must make the probe report it"
844 );
845 }
846
847 #[tokio::test]
848 async fn persist_survives_store_reopen() {
849 let dir = tempfile::tempdir().expect("tempdir");
854 let path = dir.path().to_path_buf();
855 {
856 let store = Store::open(&StoreConfig {
857 data_dir: path.clone(),
858 })
859 .expect("open store");
860 let ks = store.keyspace("keys").unwrap();
861 ks.insert_raw("carveout:closed", b"admin-did".to_vec())
862 .await
863 .unwrap();
864 ks.persist().await.unwrap();
865 }
867 let store = Store::open(&StoreConfig { data_dir: path }).expect("reopen store");
868 let ks = store.keyspace("keys").unwrap();
869 assert_eq!(
870 ks.get_raw("carveout:closed").await.unwrap().as_deref(),
871 Some(b"admin-did".as_slice()),
872 "a persisted write must survive a store reopen"
873 );
874 }
875
876 #[tokio::test]
877 async fn insert_if_absent_claims_only_once() {
878 let (store, _dir) = temp_store();
879 let ks = store.keyspace("test").unwrap();
880
881 assert!(
882 ks.insert_if_absent("k", &"first".to_string())
883 .await
884 .unwrap(),
885 "first claim must succeed"
886 );
887 assert!(
888 !ks.insert_if_absent("k", &"second".to_string())
889 .await
890 .unwrap(),
891 "second claim must be refused"
892 );
893 let got: String = ks.get("k").await.unwrap().unwrap();
894 assert_eq!(got, "first", "loser must not overwrite the stored value");
895 }
896
897 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
898 async fn insert_if_absent_under_concurrency_admits_exactly_one() {
899 let (store, _dir) = temp_store();
900 let ks = store.keyspace("test").unwrap();
901
902 let mut handles = Vec::new();
903 for i in 0..16u32 {
904 let ks = ks.clone();
905 handles.push(tokio::spawn(async move {
906 ks.insert_if_absent("contested", &format!("writer-{i}"))
907 .await
908 .unwrap()
909 }));
910 }
911 let mut winners = 0;
912 for h in handles {
913 if h.await.unwrap() {
914 winners += 1;
915 }
916 }
917 assert_eq!(winners, 1, "exactly one racing claim may win");
918 }
919
920 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
921 async fn take_raw_under_concurrency_admits_exactly_one() {
922 let (store, _dir) = temp_store();
927 store
928 .keyspace("test")
929 .unwrap()
930 .insert("token", &"refresh".to_string())
931 .await
932 .unwrap();
933
934 let mut handles = Vec::new();
935 for _ in 0..16 {
936 let ks = store.keyspace("test").unwrap();
937 handles.push(tokio::spawn(
938 async move { ks.take_raw("token").await.unwrap() },
939 ));
940 }
941 let mut claimed = 0;
942 for h in handles {
943 if h.await.unwrap().is_some() {
944 claimed += 1;
945 }
946 }
947 assert_eq!(claimed, 1, "exactly one concurrent take_raw may claim");
948 }
949
950 #[tokio::test]
951 async fn test_basic_roundtrip() {
952 let (store, _dir) = temp_store();
953 let ks = store.keyspace("test").unwrap();
954
955 #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
956 struct TestRecord {
957 id: String,
958 value: u64,
959 }
960
961 let record = TestRecord {
962 id: "test-1".into(),
963 value: 42,
964 };
965
966 ks.insert("key:test-1", &record).await.unwrap();
967 let got: TestRecord = ks.get("key:test-1").await.unwrap().unwrap();
968 assert_eq!(got, record);
969 }
970
971 #[tokio::test]
972 async fn test_prefix_iter() {
973 let (store, _dir) = temp_store();
974 let ks = store.keyspace("test").unwrap();
975
976 for i in 0..5 {
977 ks.insert_raw(format!("prefix:{i}"), format!("value-{i}").into_bytes())
978 .await
979 .unwrap();
980 }
981
982 let raw = ks.prefix_iter_raw("prefix:").await.unwrap();
983 assert_eq!(raw.len(), 5);
984 }
985
986 #[tokio::test]
987 async fn test_range_from_raw_seeks_to_lower_bound() {
988 let (store, _dir) = temp_store();
989 let ks = store.keyspace("test").unwrap();
990
991 for k in ["2026-01:a", "2026-02:b", "2026-03:c", "2026-04:d"] {
994 ks.insert_raw(k.as_bytes().to_vec(), b"v".to_vec())
995 .await
996 .unwrap();
997 }
998
999 let rows = ks.range_from_raw(b"2026-03:".to_vec()).await.unwrap();
1001 let keys: Vec<String> = rows
1002 .iter()
1003 .map(|(k, _)| String::from_utf8(k.clone()).unwrap())
1004 .collect();
1005 assert_eq!(keys, vec!["2026-03:c", "2026-04:d"]);
1006
1007 assert_eq!(ks.range_from_raw(Vec::new()).await.unwrap().len(), 4);
1009 assert!(
1011 ks.range_from_raw(b"2026-99:".to_vec())
1012 .await
1013 .unwrap()
1014 .is_empty()
1015 );
1016 }
1017
1018 #[tokio::test]
1019 async fn test_remove() {
1020 let (store, _dir) = temp_store();
1021 let ks = store.keyspace("test").unwrap();
1022
1023 ks.insert_raw("key", b"value".to_vec()).await.unwrap();
1024 assert!(ks.get_raw("key").await.unwrap().is_some());
1025
1026 ks.remove("key").await.unwrap();
1027 assert!(ks.get_raw("key").await.unwrap().is_none());
1028 }
1029
1030 #[tokio::test]
1031 async fn test_swap() {
1032 let (store, _dir) = temp_store();
1033 let ks = store.keyspace("test").unwrap();
1034
1035 ks.insert("old", &"value").await.unwrap();
1036 let swapped = ks.swap("old", "new", &"value").await.unwrap();
1037 assert!(swapped);
1038 assert!(ks.get::<String>("old").await.unwrap().is_none());
1039 assert!(ks.get::<String>("new").await.unwrap().is_some());
1040 }
1041
1042 #[tokio::test]
1043 async fn test_passthrough_mode_no_encryption() {
1044 let (store, _dir) = temp_store();
1045 let ks = store.keyspace("plain").unwrap();
1046 assert!(!ks.is_encrypted());
1047
1048 ks.insert_raw("test", b"visible".to_vec()).await.unwrap();
1049 let raw = ks.get_raw("test").await.unwrap().unwrap();
1050 assert_eq!(raw, b"visible");
1051 }
1052
1053 #[cfg(feature = "encryption")]
1054 #[tokio::test]
1055 async fn test_encrypted_roundtrip() {
1056 let (store, _dir) = temp_store();
1057 let ks = store
1058 .keyspace("encrypted")
1059 .unwrap()
1060 .with_encryption([0xAB; 32]);
1061
1062 assert!(ks.is_encrypted());
1063
1064 ks.insert_raw("raw:test", b"hello world".to_vec())
1066 .await
1067 .unwrap();
1068 let raw = ks.get_raw("raw:test").await.unwrap().unwrap();
1069 assert_eq!(raw, b"hello world");
1070
1071 ks.insert("json:test", &"encrypted value").await.unwrap();
1073 let got: String = ks.get("json:test").await.unwrap().unwrap();
1074 assert_eq!(got, "encrypted value");
1075 }
1076
1077 #[cfg(feature = "encryption")]
1083 #[tokio::test]
1084 async fn encrypted_value_cannot_be_pasted_to_another_key() {
1085 let (store, _dir) = temp_store();
1086 let key = [0x55; 32];
1087 let ks = store.keyspace("acl").unwrap().with_encryption(key);
1088
1089 ks.insert_raw("acl:victim", b"admin-row".to_vec())
1090 .await
1091 .unwrap();
1092
1093 let raw = store.keyspace("acl").unwrap();
1097 let stolen = raw.get_raw("acl:victim").await.unwrap().unwrap();
1098 raw.insert_raw("acl:attacker", stolen).await.unwrap();
1099
1100 let err = ks.get_raw("acl:attacker").await;
1103 assert!(
1104 err.is_err(),
1105 "a ciphertext pasted to a different key must fail AAD authentication"
1106 );
1107 assert_eq!(
1109 ks.get_raw("acl:victim").await.unwrap().unwrap(),
1110 b"admin-row"
1111 );
1112 }
1113
1114 #[cfg(feature = "encryption")]
1115 #[tokio::test]
1116 async fn test_encrypted_data_is_actually_encrypted_on_disk() {
1117 let (store, _dir) = temp_store();
1118 let enc_key = [0x42; 32];
1119
1120 let ks_enc = store.keyspace("secrets").unwrap().with_encryption(enc_key);
1122 ks_enc
1123 .insert_raw("test", b"plaintext secret".to_vec())
1124 .await
1125 .unwrap();
1126
1127 let ks_raw = store.keyspace("secrets").unwrap();
1129 let on_disk = ks_raw.get_raw("test").await.unwrap().unwrap();
1130
1131 assert_ne!(on_disk, b"plaintext secret");
1133 assert!(on_disk.len() >= 12 + 16 + 16);
1135
1136 let decrypted = ks_enc.get_raw("test").await.unwrap().unwrap();
1138 assert_eq!(decrypted, b"plaintext secret");
1139 }
1140
1141 #[cfg(feature = "encryption")]
1145 #[tokio::test]
1146 async fn migrate_to_encrypted_converts_legacy_plaintext() {
1147 let (store, _dir) = temp_store();
1148 let key = [0x33; 32];
1149
1150 let bare = store.keyspace("install").unwrap();
1152 bare.insert_raw("token:a", b"ephemeral-key-bytes".to_vec())
1153 .await
1154 .unwrap();
1155 bare.insert("token:b", &"json-state".to_string())
1156 .await
1157 .unwrap();
1158
1159 let migrated = bare.migrate_to_encrypted(key).await.unwrap();
1161 assert_eq!(migrated, 2, "both legacy rows must be encrypted");
1162
1163 let on_disk = bare.get_raw("token:a").await.unwrap().unwrap();
1166 assert_ne!(on_disk, b"ephemeral-key-bytes");
1167 assert!(
1168 on_disk.starts_with(b"VAE1"),
1169 "migrated row must carry the v1 encryption magic"
1170 );
1171
1172 let enc = store.keyspace("install").unwrap().with_encryption(key);
1174 assert_eq!(
1175 enc.get_raw("token:a").await.unwrap().unwrap(),
1176 b"ephemeral-key-bytes"
1177 );
1178 let b: String = enc.get("token:b").await.unwrap().unwrap();
1179 assert_eq!(b, "json-state");
1180 }
1181
1182 #[cfg(feature = "encryption")]
1186 #[tokio::test]
1187 async fn migrate_to_encrypted_is_idempotent_and_crash_safe() {
1188 let (store, _dir) = temp_store();
1189 let key = [0x44; 32];
1190
1191 let bare = store.keyspace("passkey").unwrap();
1192 bare.insert_raw("row:1", b"plaintext-one".to_vec())
1193 .await
1194 .unwrap();
1195
1196 assert_eq!(bare.migrate_to_encrypted(key).await.unwrap(), 1);
1198
1199 bare.insert_raw("row:2", b"plaintext-two".to_vec())
1202 .await
1203 .unwrap();
1204
1205 assert_eq!(bare.migrate_to_encrypted(key).await.unwrap(), 1);
1208
1209 assert_eq!(bare.migrate_to_encrypted(key).await.unwrap(), 0);
1211
1212 let enc = store.keyspace("passkey").unwrap().with_encryption(key);
1213 assert_eq!(
1214 enc.get_raw("row:1").await.unwrap().unwrap(),
1215 b"plaintext-one"
1216 );
1217 assert_eq!(
1218 enc.get_raw("row:2").await.unwrap().unwrap(),
1219 b"plaintext-two"
1220 );
1221 }
1222
1223 #[cfg(feature = "encryption")]
1227 #[tokio::test]
1228 async fn migrate_to_encrypted_rejects_encrypted_handle() {
1229 let (store, _dir) = temp_store();
1230 let enc = store
1231 .keyspace("install")
1232 .unwrap()
1233 .with_encryption([0x55; 32]);
1234 assert!(enc.migrate_to_encrypted([0x55; 32]).await.is_err());
1235 }
1236}