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 #[cfg(feature = "encryption")]
446 name: String,
447 db: fjall::Database,
451 write_lock: std::sync::Arc<std::sync::Mutex<()>>,
454 #[cfg(feature = "encryption")]
455 encryption_key: Option<std::sync::Arc<zeroize::Zeroizing<[u8; 32]>>>,
456}
457
458fn lock_writes(lock: &std::sync::Mutex<()>) -> std::sync::MutexGuard<'_, ()> {
463 lock.lock()
464 .unwrap_or_else(std::sync::PoisonError::into_inner)
465}
466
467impl LocalStore {
468 pub fn open(config: &StoreConfig) -> Result<Self, AppError> {
469 std::fs::create_dir_all(&config.data_dir).map_err(AppError::Io)?;
470 info!(path = %config.data_dir.display(), "opening store");
471 let db = fjall::Database::builder(&config.data_dir).open()?;
472 Ok(Self {
473 db,
474 write_locks: WriteLocks::default(),
475 })
476 }
477
478 pub fn keyspace(&self, name: &str) -> Result<LocalKeyspaceHandle, AppError> {
479 let keyspace = self.db.keyspace(name, KeyspaceCreateOptions::default)?;
480 let write_lock = self
481 .write_locks
482 .lock()
483 .unwrap_or_else(std::sync::PoisonError::into_inner)
484 .entry(name.to_string())
485 .or_default()
486 .clone();
487 Ok(LocalKeyspaceHandle {
488 keyspace,
489 #[cfg(feature = "encryption")]
490 name: name.to_string(),
491 db: self.db.clone(),
492 write_lock,
493 #[cfg(feature = "encryption")]
494 encryption_key: None,
495 })
496 }
497
498 pub async fn persist(&self) -> Result<(), AppError> {
499 let db = self.db.clone();
500 tokio::task::spawn_blocking(move || db.persist(PersistMode::SyncAll))
501 .await
502 .map_err(|e| AppError::Internal(format!("blocking task panicked: {e}")))??;
503 Ok(())
504 }
505}
506
507impl LocalKeyspaceHandle {
508 #[cfg(feature = "encryption")]
509 pub fn with_encryption(mut self, key: [u8; 32]) -> Self {
510 self.encryption_key = Some(std::sync::Arc::new(zeroize::Zeroizing::new(key)));
511 self
512 }
513
514 pub fn is_encrypted(&self) -> bool {
515 #[cfg(feature = "encryption")]
516 {
517 self.encryption_key.is_some()
518 }
519 #[cfg(not(feature = "encryption"))]
520 {
521 false
522 }
523 }
524
525 pub async fn persist(&self) -> Result<(), AppError> {
528 let db = self.db.clone();
529 blocking_with_timeout(move || Ok(db.persist(PersistMode::SyncAll)?)).await
530 }
531
532 pub async fn insert<V: Serialize>(
533 &self,
534 key: impl Into<Vec<u8>>,
535 value: &V,
536 ) -> Result<(), AppError> {
537 let key = key.into();
538 let bytes = serde_json::to_vec(value)?;
539 let bytes = self.maybe_encrypt(&key, bytes)?;
540 let ks = self.keyspace.clone();
541 blocking_with_timeout(move || Ok(ks.insert(key, bytes)?)).await
542 }
543
544 pub async fn get<V: DeserializeOwned + Send + 'static>(
545 &self,
546 key: impl Into<Vec<u8>>,
547 ) -> Result<Option<V>, AppError> {
548 let key = key.into();
549 let ks = self.keyspace.clone();
550 #[cfg(feature = "encryption")]
551 let enc_key = self.encryption_key.clone();
552 #[cfg(feature = "encryption")]
553 let name = self.name.clone();
554 blocking_with_timeout(move || match ks.get(&key)? {
555 Some(bytes) => {
556 #[cfg(feature = "encryption")]
557 let bytes = {
558 let k = enc_key.as_ref().map(|arc| &***arc);
559 encryption::maybe_decrypt_bytes(k, &name, &key, &bytes)?
560 };
561 #[cfg(not(feature = "encryption"))]
562 let bytes = bytes.to_vec();
563 Ok(Some(serde_json::from_slice(&bytes)?))
564 }
565 None => Ok(None),
566 })
567 .await
568 }
569
570 pub async fn remove(&self, key: impl Into<Vec<u8>>) -> Result<(), AppError> {
571 let key = key.into();
572 let ks = self.keyspace.clone();
573 blocking_with_timeout(move || Ok(ks.remove(key)?)).await
574 }
575
576 pub async fn take_raw(&self, key: impl Into<Vec<u8>>) -> Result<Option<Vec<u8>>, AppError> {
590 let key = key.into();
591 let ks = self.keyspace.clone();
592 let lock = self.write_lock.clone();
593 #[cfg(feature = "encryption")]
594 let enc_key = self.encryption_key.clone();
595 #[cfg(feature = "encryption")]
596 let name = self.name.clone();
597 blocking_with_timeout(move || {
598 let _guard = lock_writes(&lock);
599 match ks.get(&key)? {
600 Some(bytes) => {
601 ks.remove(&key)?;
602 #[cfg(feature = "encryption")]
603 let bytes = {
604 let k = enc_key.as_ref().map(|arc| &***arc);
605 encryption::maybe_decrypt_bytes(k, &name, &key, &bytes)?
606 };
607 #[cfg(not(feature = "encryption"))]
608 let bytes = bytes.to_vec();
609 Ok(Some(bytes))
610 }
611 None => Ok(None),
612 }
613 })
614 .await
615 }
616
617 pub async fn insert_raw(
618 &self,
619 key: impl Into<Vec<u8>>,
620 value: impl Into<Vec<u8>>,
621 ) -> Result<(), AppError> {
622 let key = key.into();
623 let value = self.maybe_encrypt(&key, value.into())?;
624 let ks = self.keyspace.clone();
625 blocking_with_timeout(move || Ok(ks.insert(key, value)?)).await
626 }
627
628 pub async fn get_raw(&self, key: impl Into<Vec<u8>>) -> Result<Option<Vec<u8>>, AppError> {
629 let key = key.into();
630 let ks = self.keyspace.clone();
631 #[cfg(feature = "encryption")]
632 let enc_key = self.encryption_key.clone();
633 #[cfg(feature = "encryption")]
634 let name = self.name.clone();
635 blocking_with_timeout(move || match ks.get(&key)? {
636 Some(bytes) => {
637 #[cfg(feature = "encryption")]
638 let bytes = {
639 let k = enc_key.as_ref().map(|arc| &***arc);
640 encryption::maybe_decrypt_bytes(k, &name, &key, &bytes)?
641 };
642 #[cfg(not(feature = "encryption"))]
643 let bytes = bytes.to_vec();
644 Ok(Some(bytes))
645 }
646 None => Ok(None),
647 })
648 .await
649 }
650
651 pub async fn prefix_iter_raw(
652 &self,
653 prefix: impl Into<Vec<u8>>,
654 ) -> Result<Vec<RawKvPair>, AppError> {
655 let prefix = prefix.into();
656 let ks = self.keyspace.clone();
657 #[cfg(feature = "encryption")]
658 let enc_key = self.encryption_key.clone();
659 #[cfg(feature = "encryption")]
660 let name = self.name.clone();
661 blocking_with_timeout(move || {
662 let mut results = Vec::new();
663 for guard in ks.prefix(&prefix) {
664 let (key, value) = guard.into_inner()?;
665 #[cfg(feature = "encryption")]
666 let value = {
667 let k = enc_key.as_ref().map(|arc| &***arc);
668 encryption::maybe_decrypt_bytes(k, &name, &key, &value)?
669 };
670 #[cfg(not(feature = "encryption"))]
671 let value = value.to_vec();
672 results.push((key.to_vec(), value));
673 }
674 Ok(results)
675 })
676 .await
677 }
678
679 pub async fn range_from_raw(
682 &self,
683 from: impl Into<Vec<u8>>,
684 ) -> Result<Vec<RawKvPair>, AppError> {
685 let from = from.into();
686 let ks = self.keyspace.clone();
687 #[cfg(feature = "encryption")]
688 let enc_key = self.encryption_key.clone();
689 #[cfg(feature = "encryption")]
690 let name = self.name.clone();
691 blocking_with_timeout(move || {
692 let mut results = Vec::new();
693 for guard in ks.range(from..) {
694 let (key, value) = guard.into_inner()?;
695 #[cfg(feature = "encryption")]
696 let value = {
697 let k = enc_key.as_ref().map(|arc| &***arc);
698 encryption::maybe_decrypt_bytes(k, &name, &key, &value)?
699 };
700 #[cfg(not(feature = "encryption"))]
701 let value = value.to_vec();
702 results.push((key.to_vec(), value));
703 }
704 Ok(results)
705 })
706 .await
707 }
708
709 pub async fn prefix_keys(&self, prefix: impl Into<Vec<u8>>) -> Result<Vec<Vec<u8>>, AppError> {
710 let prefix = prefix.into();
711 let ks = self.keyspace.clone();
712 blocking_with_timeout(move || {
713 let mut results = Vec::new();
714 for guard in ks.prefix(&prefix) {
715 let (key, _value) = guard.into_inner()?;
716 results.push(key.to_vec());
717 }
718 Ok(results)
719 })
720 .await
721 }
722
723 pub async fn approximate_len(&self) -> Result<usize, AppError> {
724 let ks = self.keyspace.clone();
725 blocking_with_timeout(move || Ok(ks.approximate_len())).await
726 }
727
728 pub async fn swap<V: Serialize>(
729 &self,
730 old_key: impl Into<Vec<u8>>,
731 new_key: impl Into<Vec<u8>>,
732 value: &V,
733 ) -> Result<bool, AppError> {
734 let old_key = old_key.into();
735 let new_key = new_key.into();
736 let bytes = serde_json::to_vec(value)?;
737 let bytes = self.maybe_encrypt(&new_key, bytes)?;
739 let ks = self.keyspace.clone();
740 let lock = self.write_lock.clone();
741 blocking_with_timeout(move || {
742 let _guard = lock_writes(&lock);
743 if ks.contains_key(&new_key)? {
744 return Ok(false);
745 }
746 ks.insert(&new_key, bytes)?;
747 ks.remove(&old_key)?;
748 Ok(true)
749 })
750 .await
751 }
752
753 pub async fn insert_if_absent<V: Serialize>(
757 &self,
758 key: impl Into<Vec<u8>>,
759 value: &V,
760 ) -> Result<bool, AppError> {
761 let key = key.into();
762 let bytes = serde_json::to_vec(value)?;
763 self.insert_bytes_if_absent(key, bytes).await
764 }
765
766 pub async fn insert_raw_if_absent(
769 &self,
770 key: impl Into<Vec<u8>>,
771 value: impl Into<Vec<u8>>,
772 ) -> Result<bool, AppError> {
773 self.insert_bytes_if_absent(key.into(), value.into()).await
774 }
775
776 async fn insert_bytes_if_absent(&self, key: Vec<u8>, bytes: Vec<u8>) -> Result<bool, AppError> {
780 let bytes = self.maybe_encrypt(&key, bytes)?;
781 let ks = self.keyspace.clone();
782 let lock = self.write_lock.clone();
783 blocking_with_timeout(move || {
784 let _guard = lock_writes(&lock);
785 if ks.contains_key(&key)? {
786 return Ok(false);
787 }
788 ks.insert(&key, bytes)?;
789 Ok(true)
790 })
791 .await
792 }
793
794 fn maybe_encrypt(&self, store_key: &[u8], plaintext: Vec<u8>) -> Result<Vec<u8>, AppError> {
795 #[cfg(feature = "encryption")]
796 {
797 match self.encryption_key.as_ref().map(|arc| &***arc) {
798 Some(key) => encryption::encrypt_value(key, &self.name, store_key, &plaintext),
799 None => Ok(plaintext),
800 }
801 }
802 #[cfg(not(feature = "encryption"))]
803 {
804 let _ = store_key;
805 Ok(plaintext)
806 }
807 }
808}
809
810#[cfg(test)]
811mod tests {
812 use super::*;
813
814 fn temp_store() -> (Store, tempfile::TempDir) {
815 let dir = tempfile::tempdir().expect("failed to create temp dir");
816 let config = StoreConfig {
817 data_dir: dir.path().to_path_buf(),
818 };
819 let store = Store::open(&config).expect("failed to open store");
820 (store, dir)
821 }
822
823 #[test]
824 fn local_store_exists_ignores_a_bare_directory() {
825 let dir = tempfile::tempdir().expect("tempdir");
830 assert!(
831 !local_store_exists(dir.path()),
832 "an empty directory holds no store"
833 );
834
835 std::fs::write(dir.path().join("did.jsonl"), b"{}").expect("write stray file");
836 assert!(
837 !local_store_exists(dir.path()),
838 "a non-empty directory without fjall's marker still holds no store"
839 );
840
841 assert!(
842 !local_store_exists(&dir.path().join("does-not-exist")),
843 "an absent directory holds no store"
844 );
845 }
846
847 #[test]
848 fn local_store_exists_sees_an_opened_store() {
849 let (_store, dir) = temp_store();
850 assert!(
851 local_store_exists(dir.path()),
852 "opening a store must make the probe report it"
853 );
854 }
855
856 #[tokio::test]
857 async fn persist_survives_store_reopen() {
858 let dir = tempfile::tempdir().expect("tempdir");
863 let path = dir.path().to_path_buf();
864 {
865 let store = Store::open(&StoreConfig {
866 data_dir: path.clone(),
867 })
868 .expect("open store");
869 let ks = store.keyspace("keys").unwrap();
870 ks.insert_raw("carveout:closed", b"admin-did".to_vec())
871 .await
872 .unwrap();
873 ks.persist().await.unwrap();
874 }
876 let store = Store::open(&StoreConfig { data_dir: path }).expect("reopen store");
877 let ks = store.keyspace("keys").unwrap();
878 assert_eq!(
879 ks.get_raw("carveout:closed").await.unwrap().as_deref(),
880 Some(b"admin-did".as_slice()),
881 "a persisted write must survive a store reopen"
882 );
883 }
884
885 #[tokio::test]
886 async fn insert_if_absent_claims_only_once() {
887 let (store, _dir) = temp_store();
888 let ks = store.keyspace("test").unwrap();
889
890 assert!(
891 ks.insert_if_absent("k", &"first".to_string())
892 .await
893 .unwrap(),
894 "first claim must succeed"
895 );
896 assert!(
897 !ks.insert_if_absent("k", &"second".to_string())
898 .await
899 .unwrap(),
900 "second claim must be refused"
901 );
902 let got: String = ks.get("k").await.unwrap().unwrap();
903 assert_eq!(got, "first", "loser must not overwrite the stored value");
904 }
905
906 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
907 async fn insert_if_absent_under_concurrency_admits_exactly_one() {
908 let (store, _dir) = temp_store();
909 let ks = store.keyspace("test").unwrap();
910
911 let mut handles = Vec::new();
912 for i in 0..16u32 {
913 let ks = ks.clone();
914 handles.push(tokio::spawn(async move {
915 ks.insert_if_absent("contested", &format!("writer-{i}"))
916 .await
917 .unwrap()
918 }));
919 }
920 let mut winners = 0;
921 for h in handles {
922 if h.await.unwrap() {
923 winners += 1;
924 }
925 }
926 assert_eq!(winners, 1, "exactly one racing claim may win");
927 }
928
929 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
930 async fn take_raw_under_concurrency_admits_exactly_one() {
931 let (store, _dir) = temp_store();
936 store
937 .keyspace("test")
938 .unwrap()
939 .insert("token", &"refresh".to_string())
940 .await
941 .unwrap();
942
943 let mut handles = Vec::new();
944 for _ in 0..16 {
945 let ks = store.keyspace("test").unwrap();
946 handles.push(tokio::spawn(
947 async move { ks.take_raw("token").await.unwrap() },
948 ));
949 }
950 let mut claimed = 0;
951 for h in handles {
952 if h.await.unwrap().is_some() {
953 claimed += 1;
954 }
955 }
956 assert_eq!(claimed, 1, "exactly one concurrent take_raw may claim");
957 }
958
959 #[tokio::test]
960 async fn test_basic_roundtrip() {
961 let (store, _dir) = temp_store();
962 let ks = store.keyspace("test").unwrap();
963
964 #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
965 struct TestRecord {
966 id: String,
967 value: u64,
968 }
969
970 let record = TestRecord {
971 id: "test-1".into(),
972 value: 42,
973 };
974
975 ks.insert("key:test-1", &record).await.unwrap();
976 let got: TestRecord = ks.get("key:test-1").await.unwrap().unwrap();
977 assert_eq!(got, record);
978 }
979
980 #[tokio::test]
981 async fn test_prefix_iter() {
982 let (store, _dir) = temp_store();
983 let ks = store.keyspace("test").unwrap();
984
985 for i in 0..5 {
986 ks.insert_raw(format!("prefix:{i}"), format!("value-{i}").into_bytes())
987 .await
988 .unwrap();
989 }
990
991 let raw = ks.prefix_iter_raw("prefix:").await.unwrap();
992 assert_eq!(raw.len(), 5);
993 }
994
995 #[tokio::test]
996 async fn test_range_from_raw_seeks_to_lower_bound() {
997 let (store, _dir) = temp_store();
998 let ks = store.keyspace("test").unwrap();
999
1000 for k in ["2026-01:a", "2026-02:b", "2026-03:c", "2026-04:d"] {
1003 ks.insert_raw(k.as_bytes().to_vec(), b"v".to_vec())
1004 .await
1005 .unwrap();
1006 }
1007
1008 let rows = ks.range_from_raw(b"2026-03:".to_vec()).await.unwrap();
1010 let keys: Vec<String> = rows
1011 .iter()
1012 .map(|(k, _)| String::from_utf8(k.clone()).unwrap())
1013 .collect();
1014 assert_eq!(keys, vec!["2026-03:c", "2026-04:d"]);
1015
1016 assert_eq!(ks.range_from_raw(Vec::new()).await.unwrap().len(), 4);
1018 assert!(
1020 ks.range_from_raw(b"2026-99:".to_vec())
1021 .await
1022 .unwrap()
1023 .is_empty()
1024 );
1025 }
1026
1027 #[tokio::test]
1028 async fn test_remove() {
1029 let (store, _dir) = temp_store();
1030 let ks = store.keyspace("test").unwrap();
1031
1032 ks.insert_raw("key", b"value".to_vec()).await.unwrap();
1033 assert!(ks.get_raw("key").await.unwrap().is_some());
1034
1035 ks.remove("key").await.unwrap();
1036 assert!(ks.get_raw("key").await.unwrap().is_none());
1037 }
1038
1039 #[tokio::test]
1040 async fn test_swap() {
1041 let (store, _dir) = temp_store();
1042 let ks = store.keyspace("test").unwrap();
1043
1044 ks.insert("old", &"value").await.unwrap();
1045 let swapped = ks.swap("old", "new", &"value").await.unwrap();
1046 assert!(swapped);
1047 assert!(ks.get::<String>("old").await.unwrap().is_none());
1048 assert!(ks.get::<String>("new").await.unwrap().is_some());
1049 }
1050
1051 #[tokio::test]
1052 async fn test_passthrough_mode_no_encryption() {
1053 let (store, _dir) = temp_store();
1054 let ks = store.keyspace("plain").unwrap();
1055 assert!(!ks.is_encrypted());
1056
1057 ks.insert_raw("test", b"visible".to_vec()).await.unwrap();
1058 let raw = ks.get_raw("test").await.unwrap().unwrap();
1059 assert_eq!(raw, b"visible");
1060 }
1061
1062 #[cfg(feature = "encryption")]
1063 #[tokio::test]
1064 async fn test_encrypted_roundtrip() {
1065 let (store, _dir) = temp_store();
1066 let ks = store
1067 .keyspace("encrypted")
1068 .unwrap()
1069 .with_encryption([0xAB; 32]);
1070
1071 assert!(ks.is_encrypted());
1072
1073 ks.insert_raw("raw:test", b"hello world".to_vec())
1075 .await
1076 .unwrap();
1077 let raw = ks.get_raw("raw:test").await.unwrap().unwrap();
1078 assert_eq!(raw, b"hello world");
1079
1080 ks.insert("json:test", &"encrypted value").await.unwrap();
1082 let got: String = ks.get("json:test").await.unwrap().unwrap();
1083 assert_eq!(got, "encrypted value");
1084 }
1085
1086 #[cfg(feature = "encryption")]
1092 #[tokio::test]
1093 async fn encrypted_value_cannot_be_pasted_to_another_key() {
1094 let (store, _dir) = temp_store();
1095 let key = [0x55; 32];
1096 let ks = store.keyspace("acl").unwrap().with_encryption(key);
1097
1098 ks.insert_raw("acl:victim", b"admin-row".to_vec())
1099 .await
1100 .unwrap();
1101
1102 let raw = store.keyspace("acl").unwrap();
1106 let stolen = raw.get_raw("acl:victim").await.unwrap().unwrap();
1107 raw.insert_raw("acl:attacker", stolen).await.unwrap();
1108
1109 let err = ks.get_raw("acl:attacker").await;
1112 assert!(
1113 err.is_err(),
1114 "a ciphertext pasted to a different key must fail AAD authentication"
1115 );
1116 assert_eq!(
1118 ks.get_raw("acl:victim").await.unwrap().unwrap(),
1119 b"admin-row"
1120 );
1121 }
1122
1123 #[cfg(feature = "encryption")]
1124 #[tokio::test]
1125 async fn test_encrypted_data_is_actually_encrypted_on_disk() {
1126 let (store, _dir) = temp_store();
1127 let enc_key = [0x42; 32];
1128
1129 let ks_enc = store.keyspace("secrets").unwrap().with_encryption(enc_key);
1131 ks_enc
1132 .insert_raw("test", b"plaintext secret".to_vec())
1133 .await
1134 .unwrap();
1135
1136 let ks_raw = store.keyspace("secrets").unwrap();
1138 let on_disk = ks_raw.get_raw("test").await.unwrap().unwrap();
1139
1140 assert_ne!(on_disk, b"plaintext secret");
1142 assert!(on_disk.len() >= 12 + 16 + 16);
1144
1145 let decrypted = ks_enc.get_raw("test").await.unwrap().unwrap();
1147 assert_eq!(decrypted, b"plaintext secret");
1148 }
1149
1150 #[cfg(feature = "encryption")]
1154 #[tokio::test]
1155 async fn migrate_to_encrypted_converts_legacy_plaintext() {
1156 let (store, _dir) = temp_store();
1157 let key = [0x33; 32];
1158
1159 let bare = store.keyspace("install").unwrap();
1161 bare.insert_raw("token:a", b"ephemeral-key-bytes".to_vec())
1162 .await
1163 .unwrap();
1164 bare.insert("token:b", &"json-state".to_string())
1165 .await
1166 .unwrap();
1167
1168 let migrated = bare.migrate_to_encrypted(key).await.unwrap();
1170 assert_eq!(migrated, 2, "both legacy rows must be encrypted");
1171
1172 let on_disk = bare.get_raw("token:a").await.unwrap().unwrap();
1175 assert_ne!(on_disk, b"ephemeral-key-bytes");
1176 assert!(
1177 on_disk.starts_with(b"VAE1"),
1178 "migrated row must carry the v1 encryption magic"
1179 );
1180
1181 let enc = store.keyspace("install").unwrap().with_encryption(key);
1183 assert_eq!(
1184 enc.get_raw("token:a").await.unwrap().unwrap(),
1185 b"ephemeral-key-bytes"
1186 );
1187 let b: String = enc.get("token:b").await.unwrap().unwrap();
1188 assert_eq!(b, "json-state");
1189 }
1190
1191 #[cfg(feature = "encryption")]
1195 #[tokio::test]
1196 async fn migrate_to_encrypted_is_idempotent_and_crash_safe() {
1197 let (store, _dir) = temp_store();
1198 let key = [0x44; 32];
1199
1200 let bare = store.keyspace("passkey").unwrap();
1201 bare.insert_raw("row:1", b"plaintext-one".to_vec())
1202 .await
1203 .unwrap();
1204
1205 assert_eq!(bare.migrate_to_encrypted(key).await.unwrap(), 1);
1207
1208 bare.insert_raw("row:2", b"plaintext-two".to_vec())
1211 .await
1212 .unwrap();
1213
1214 assert_eq!(bare.migrate_to_encrypted(key).await.unwrap(), 1);
1217
1218 assert_eq!(bare.migrate_to_encrypted(key).await.unwrap(), 0);
1220
1221 let enc = store.keyspace("passkey").unwrap().with_encryption(key);
1222 assert_eq!(
1223 enc.get_raw("row:1").await.unwrap().unwrap(),
1224 b"plaintext-one"
1225 );
1226 assert_eq!(
1227 enc.get_raw("row:2").await.unwrap().unwrap(),
1228 b"plaintext-two"
1229 );
1230 }
1231
1232 #[cfg(feature = "encryption")]
1236 #[tokio::test]
1237 async fn migrate_to_encrypted_rejects_encrypted_handle() {
1238 let (store, _dir) = temp_store();
1239 let enc = store
1240 .keyspace("install")
1241 .unwrap()
1242 .with_encryption([0x55; 32]);
1243 assert!(enc.migrate_to_encrypted([0x55; 32]).await.is_err());
1244 }
1245}